This commit is contained in:
2026-08-05 09:11:26 +08:00
parent d544bbd0c5
commit 3791fee285
45 changed files with 1187 additions and 126 deletions
@@ -3,13 +3,30 @@ package com.baiyue.firstleg.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.RequestParam;
import java.nio.charset.StandardCharsets;
import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("FBA计费OpenAPI契约")
class FbaPricingContractTest {
@Test
@DisplayName("只有客户规则列表允许省略企业参数")
void onlyCustomerRuleListAllowsOptionalTenant() {
Method rules = method("rules");
assertThat(rules.getParameterTypes()[1]).isEqualTo(Long.class);
assertThat(rules.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
for (String scopedMethod : new String[] {"rule", "createRule", "retireRule", "rates",
"rate", "createRate", "retireRate"}) {
Method method = method(scopedMethod);
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
}
}
@Test
@DisplayName("契约声明自动发布、已发布费用调整和幂等头")
void documentsPricingLifecycle() throws Exception {
@@ -28,5 +45,26 @@ class FbaPricingContractTest {
.doesNotContain("应收")
.doesNotContain("应付")
.doesNotContain("待客户确认");
String customerRules = openapi.substring(
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules:"),
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules/versions:"));
assertThat(customerRules)
.contains("OptionalTenantId")
.contains("name: ruleCode")
.doesNotContain("#/components/parameters/Page")
.doesNotContain("#/components/parameters/Size")
.doesNotContain("name: customerName")
.doesNotContain("name: chargeMode")
.doesNotContain("name: status");
assertThat(openapi)
.contains("OptionalTenantId: {name: tenantId, in: query, required: false")
.contains("schema: {type: string, pattern: '^[1-9][0-9]*$'}")
.doesNotContain("format: int64");
}
private static Method method(String name) {
return java.util.Arrays.stream(PlatformFbaMasterDataController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(name))
.findFirst().orElseThrow();
}
}
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.dto.InboundOrderQuery;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
@@ -15,21 +16,40 @@ class InboundOrderControllerContractTest {
@Test
void shouldDeclareReadOnlyPermissions() throws Exception {
assertAuthority("page", "hasAuthority('platform:firstleg:inbound:list')",
AuthenticatedUser.class, long.class, InboundOrderQuery.class);
AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
assertAuthority("detail", "hasAuthority('platform:firstleg:inbound:view')",
AuthenticatedUser.class, long.class, long.class);
Method page = PlatformInboundOrderController.class.getDeclaredMethod(
"page", AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
Method detail = PlatformInboundOrderController.class.getDeclaredMethod(
"detail", AuthenticatedUser.class, long.class, long.class);
assertThat(detail.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
}
@Test
void shouldDocumentOnlyV2TabsAndStringLongIds() throws Exception {
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
String inboundList = openapi.substring(
openapi.indexOf("/api/platform/inbounds:"),
openapi.indexOf("/api/platform/inbounds/{inboundId}:"));
String taskList = openapi.substring(
openapi.indexOf("/api/platform/v1/first-leg/tasks:"),
openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:"));
assertThat(openapi)
.contains("/api/platform/inbounds:")
.contains("/api/platform/inbounds/{inboundId}:")
.contains("required: false")
.contains("InboundOrderSummary:")
.contains("required: [id, tenantId]")
.contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:")
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]")
.contains("platform:firstleg:inbound:list")
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
assertThat(taskList).doesNotContain("InboundOrderPageEnvelope");
}
private static void assertAuthority(String methodName, String value, Class<?>... types)
@@ -0,0 +1,84 @@
package com.baiyue.firstleg.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
import com.baiyue.firstleg.mapper.CarrierFbaRateMapper;
import com.baiyue.firstleg.mapper.CustomerChargeRuleMapper;
import com.baiyue.firstleg.mapper.InboundFeeItemMapper;
import com.baiyue.firstleg.mapper.InboundFeeMapper;
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
import com.baiyue.firstleg.mapper.InboundPricingIdempotencyMapper;
import com.baiyue.firstleg.mapper.InboundPricingMapper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.util.Collection;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class FbaPricingRepositoryRuleListTest {
@BeforeAll
static void initializeEntityMetadata() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"),
CustomerChargeRuleEntity.class);
}
@Test
void platformRuleListOmitsTenantConditionWhenTenantIsAbsent() {
assertThat(queryValues(null, null)).isEmpty();
}
@Test
void platformRuleListKeepsTenantAndRuleCodeFiltersWhenProvided() {
assertThat(queryValues(10001L, "CR-1")).containsExactlyInAnyOrder(10001L, "CR-1");
}
@Test
void internalVersionCalculationKeepsTenantAndRuleCodeFilters() {
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
CustomerChargeRuleEntity current = new CustomerChargeRuleEntity();
current.setRuleVersion(3);
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(current));
FbaPricingRepository repository = repository(rules);
int result = repository.nextRuleVersion(10001L, "CR-1");
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaQueryWrapper<CustomerChargeRuleEntity>> wrapper =
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(rules).selectList(wrapper.capture());
wrapper.getValue().getSqlSegment();
assertThat(wrapper.getValue().getParamNameValuePairs().values())
.containsExactlyInAnyOrder(10001L, "CR-1");
assertThat(result).isEqualTo(4);
}
private static Collection<Object> queryValues(Long tenantId, String ruleCode) {
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
FbaPricingRepository repository = repository(rules);
repository.listRules(tenantId, ruleCode);
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaQueryWrapper<CustomerChargeRuleEntity>> wrapper =
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(rules).selectList(wrapper.capture());
wrapper.getValue().getSqlSegment();
return wrapper.getValue().getParamNameValuePairs().values();
}
private static FbaPricingRepository repository(CustomerChargeRuleMapper rules) {
return new FbaPricingRepository(rules, mock(CarrierFbaRateMapper.class),
mock(InboundPricingMapper.class), mock(InboundFeeMapper.class), mock(InboundFeeItemMapper.class),
mock(InboundPricingIdempotencyMapper.class), mock(InboundOutboxMapper.class));
}
}
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.firstleg.dto.InboundOrderQuery;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
import com.baiyue.firstleg.mapper.InboundFileMapper;
import com.baiyue.firstleg.mapper.InboundInternalNoteMapper;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
@@ -73,8 +74,10 @@ class InboundOrderRepositoryTest {
Page<InboundOrderEntity> page = new Page<>(1, 20);
InboundOrderEntity first = new InboundOrderEntity();
first.setId(1L);
first.setTenantId(10001L);
InboundOrderEntity second = new InboundOrderEntity();
second.setId(2L);
second.setTenantId(10001L);
page.setRecords(java.util.List.of(first, second));
when(orders.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
when(pallets.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
@@ -104,12 +107,85 @@ class InboundOrderRepositoryTest {
.contains("ORDER BY inbound_id ASC,allocation_no ASC");
}
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void shouldQueryAndAggregateCrossTenantPageByInboundOwnership() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), InboundOrderEntity.class);
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
InboundTransportSnapshotEntity.class);
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
InboundPalletAllocationEntity.class);
InboundOrderMapper orders = mock(InboundOrderMapper.class);
InboundTransportSnapshotMapper transports = mock(InboundTransportSnapshotMapper.class);
InboundPalletAllocationMapper pallets = mock(InboundPalletAllocationMapper.class);
Page<InboundOrderEntity> page = new Page<>(1, 20);
page.setRecords(java.util.List.of(order(1L, 10001L), order(2L, 10002L)));
when(orders.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
when(transports.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
transport(1L, 10001L, "T1"),
transport(2L, 10002L, "T2"),
transport(1L, 10002L, "WRONG")));
when(pallets.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
allocation(1L, 10001L, 1, "A1"),
allocation(2L, 10002L, 1, "B1"),
allocation(1L, 10002L, 2, "WRONG")));
InboundOrderRepository repository = new InboundOrderRepository(
orders, transports, pallets, mock(InboundFileMapper.class),
mock(InboundInternalNoteMapper.class), mock(InboundStatusHistoryMapper.class));
var result = repository.page(null, new InboundOrderQuery(
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, 1, 20));
assertThat(result.records()).extracting(record -> record.tenantId())
.containsExactly(10001L, 10002L);
assertThat(result.records()).extracting(record -> record.loadingStatus())
.containsExactly("T1", "T2");
assertThat(result.records()).extracting(record -> record.locationCodes())
.containsExactly("A1", "B1");
ArgumentCaptor<Wrapper<InboundOrderEntity>> orderCaptor = ArgumentCaptor.forClass(Wrapper.class);
verify(orders).selectPage(any(Page.class), orderCaptor.capture());
assertThat(orderCaptor.getValue().getCustomSqlSegment()).doesNotContain("tenant_id");
ArgumentCaptor<Wrapper<InboundTransportSnapshotEntity>> transportCaptor =
ArgumentCaptor.forClass(Wrapper.class);
verify(transports).selectList(transportCaptor.capture());
assertThat(transportCaptor.getValue().getCustomSqlSegment())
.doesNotContain("tenant_id")
.contains("inbound_id IN");
}
private static InboundPalletAllocationEntity allocation(
long inboundId, int allocationNo, String locationCode) {
return allocation(inboundId, 10001L, allocationNo, locationCode);
}
private static InboundPalletAllocationEntity allocation(
long inboundId, long tenantId, int allocationNo, String locationCode) {
InboundPalletAllocationEntity entity = new InboundPalletAllocationEntity();
entity.setInboundId(inboundId);
entity.setTenantId(tenantId);
entity.setAllocationNo(allocationNo);
entity.setLocationCode(locationCode);
return entity;
}
private static InboundOrderEntity order(long id, long tenantId) {
InboundOrderEntity entity = new InboundOrderEntity();
entity.setId(id);
entity.setTenantId(tenantId);
return entity;
}
private static InboundTransportSnapshotEntity transport(
long inboundId, long tenantId, String loadingStatus) {
InboundTransportSnapshotEntity entity = new InboundTransportSnapshotEntity();
entity.setInboundId(inboundId);
entity.setTenantId(tenantId);
entity.setLoadingStatus(loadingStatus);
return entity;
}
}
@@ -16,12 +16,27 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("FBA计费基础资料版本")
class FbaMasterDataServiceTest {
private static final LocalDateTime AUGUST = LocalDateTime.of(2026, 8, 1, 0, 0);
@Test
@DisplayName("平台列表不传企业时委托仓储查询全部企业规则")
void listsRulesAcrossTenantsWithoutTenantFilter() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
List<CustomerChargeRuleEntity> expected = List.of(activeRule(AUGUST, null));
when(repository.listRules(null, "CR-1")).thenReturn(expected);
List<CustomerChargeRuleEntity> result =
new FbaMasterDataService(repository).listRules(null, "CR-1");
assertThat(result).isSameAs(expected);
verify(repository).listRules(null, "CR-1");
}
@Test
@DisplayName("半开有效期首尾相接不视为重叠")
void acceptsAdjacentHalfOpenPeriods() {
@@ -21,6 +21,17 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class InboundOrderQueryServiceTest {
@Test
void shouldQueryAllPlatformTenantsWhenTenantFilterIsMissing() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
InboundOrderQuery query = query(null, null, null, null);
PageResult<InboundOrderSummary> expected = new PageResult<>(List.of(), 1, 20, 0, 0);
when(repository.page(null, query)).thenReturn(expected);
assertThat(new InboundOrderQueryService(repository).page(null, query)).isSameAs(expected);
verify(repository).page(null, query);
}
@Test
void shouldKeepV2TabMappingWithoutCustomerConfirmation() {
assertThat(InboundListTab.WAIT_MEASURE.processStatuses())