feat(firstleg): expose smart-loading channel directory

This commit is contained in:
2026-08-31 14:54:00 +08:00
parent ea615b864a
commit 0122ad314b
9 changed files with 152 additions and 1 deletions
@@ -12,11 +12,14 @@ class PlatformSmartLoadingControllerContractTest {
authority("draftByBooking","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class);
authority("detail","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class);
authority("candidates","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class,long.class);
authority("inboundCandidates","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class,long.class);
authority("inboundCandidates","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class,long.class);
authority("channels","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class);
}
@Test void openApiRequiresBookingAndWarehouseForCandidates() throws Exception {
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
assertThat(yaml).contains("/api/platform/v1/first-leg/smart-loading/candidates:")
.contains("/api/platform/v1/first-leg/smart-loading/channels:")
.contains("零候选或零可用柜次的有效渠道仍返回")
.contains("name: bookingId, in: query, required: true")
.contains("name: warehouseId, in: query, required: true");
}
@@ -69,4 +69,19 @@ class SmartLoadingCandidateMapperContractTest {
.contains("GROUP BY tenant_id,measurement_id,inbound_detail_id")
.contains("FOR UPDATE");
}
@Test
void channelContainerStatisticsCountOnlyActiveOrchestratableContainers() throws Exception {
Method method = SmartLoadingCandidateMapper.class.getDeclaredMethod(
"channelContainerCounts", long.class);
String sql = String.join(" ", method.getAnnotation(Select.class).value());
assertThat(sql)
.contains("b.record_status='ACTIVE'")
.contains("b.warehouse_id=#{warehouseId}")
.contains("p.id IS NULL OR p.plan_status IN ('EDITING','LOADING')")
.contains("COUNT(DISTINCT b.id) availableContainerCount")
.contains("GROUP BY b.channel_code")
.doesNotContain("'COMPLETED'");
}
}
@@ -22,6 +22,9 @@ import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow;
import com.baiyue.firstleg.split.service.SplitPreloadService;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
@@ -73,6 +76,7 @@ class SmartLoadingServiceTest {
@MockBean private LoadingModelBridgeService loadingModelBridge;
@MockBean private SplitPreloadService splitPreloadService;
@MockBean private ColoadGroupAssignmentPort coloadGroups;
@MockBean private PriceBookQueryUseCase priceBooks;
@MockBean private FirstlegDocumentNumberGenerator documentNumbers;
@Resource private SmartLoadingService service;
@@ -139,6 +143,40 @@ class SmartLoadingServiceTest {
});
}
@Test
void channelDirectoryKeepsActiveZeroStatChannelAndAggregatesCandidateAndContainerFacts() {
ChannelRow maxTruck = channel("MATSON_MAX_TRUCK", "美森加班海卡");
ChannelRow clxTruck = channel("MATSON_CLX_TRUCK", "美森正班海卡");
Candidate first = candidate(9L, 10L, 20L, 30L, 4);
Candidate secondDetail = candidate(9L, 10L, 21L, 30L, 6);
Candidate secondInbound = candidate(19L, 11L, 22L, 31L, 5);
first = withChannel(first, "MATSON_MAX_TRUCK", "旧渠道快照名称");
secondDetail = withChannel(secondDetail, "MATSON_MAX_TRUCK", "旧渠道快照名称");
secondInbound = withChannel(secondInbound, "MATSON_MAX_TRUCK", "旧渠道快照名称");
ChannelContainerCountRow count = new ChannelContainerCountRow();
count.setChannelCode("MATSON_MAX_TRUCK"); count.setAvailableContainerCount(2L);
when(priceBooks.activeVersionId()).thenReturn(88L);
when(priceBooks.channels(88L, 100L)).thenReturn(List.of(maxTruck, clxTruck));
when(repository.candidates(100L)).thenReturn(List.of(first, secondDetail, secondInbound));
when(repository.channelContainerCounts(100L)).thenReturn(List.of(count));
var result = service.channels(100L);
assertThat(result).hasSize(2);
assertThat(result.get(0)).satisfies(channel -> {
assertThat(channel.channelName()).isEqualTo("美森加班海卡");
assertThat(channel.candidateInboundCount()).isEqualTo(2);
assertThat(channel.availablePackageCount()).isEqualTo(15);
assertThat(channel.availableContainerCount()).isEqualTo(2);
});
assertThat(result.get(1)).satisfies(channel -> {
assertThat(channel.channelCode()).isEqualTo("MATSON_CLX_TRUCK");
assertThat(channel.candidateInboundCount()).isZero();
assertThat(channel.availablePackageCount()).isZero();
assertThat(channel.availableContainerCount()).isZero();
});
}
@Test
void exposesWholeColoadGroupFactsOnEveryMemberCandidate() {
Candidate first = candidate(9L, 10L, 20L, 30L, 4);
@@ -1297,6 +1335,21 @@ class SmartLoadingServiceTest {
new BigDecimal("0.5"), null, null, null, null, null, null);
}
private static Candidate withChannel(Candidate value, String channelCode, String channelName) {
return new Candidate(value.accountId(), value.inboundId(), value.inboundDetailId(), value.orderId(),
value.orderNo(), value.inboundCode(), value.warehouseId(), channelCode, channelName,
value.destinationWarehouseId(), value.destinationWarehouseCode(), value.destinationWarehouseName(),
value.customerName(), value.goodsName(), value.availablePackageCount(), value.unitGrossWeightKg(),
value.unitNetWeightKg(), value.unitVolumeCbm(), value.coLoadGroupId(), value.groupNo(),
value.memberCount(), value.groupTotalBoxes(), value.locked(), value.blockReason());
}
private static ChannelRow channel(String channelCode, String channelName) {
ChannelRow value = new ChannelRow();
value.setChannelCode(channelCode); value.setChannelName(channelName);
return value;
}
private static GroupSnapshot group(String status, Long lockedPlanId, Member... members) {
GroupSnapshot group = mock(GroupSnapshot.class);
when(group.groupId()).thenReturn(700L);