fix: scope smart loading recommendations
This commit is contained in:
@@ -76,7 +76,8 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
public record RemoveGroupRequest(@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
public record RecommendRequest(@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
@NotBlank @Size(max=128) String operationId,
|
||||
@Positive Long customerId, @Size(max=64) String inboundCode) {}
|
||||
public record AdjustRequest(@NotNull @Positive Integer packageCount,@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
@NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {}
|
||||
public record DeleteRequest(@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
|
||||
@@ -106,9 +106,14 @@ public class SmartLoadingService {
|
||||
return grouped.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList();
|
||||
}
|
||||
|
||||
private List<InboundCandidate> recommendationCandidates(ContainerBookingEntity booking) {
|
||||
private List<InboundCandidate> recommendationCandidates(ContainerBookingEntity booking,
|
||||
Long customerId, String inboundCode) {
|
||||
List<Candidate> values = repository.candidates(null).stream()
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate)).toList();
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate))
|
||||
.filter(candidate -> customerId == null || Objects.equals(customerId, candidate.accountId()))
|
||||
.filter(candidate -> !StringUtils.hasText(inboundCode)
|
||||
|| inboundCode.trim().equals(candidate.inboundCode()))
|
||||
.toList();
|
||||
Map<Long, List<Candidate>> grouped = enrichColoadCandidates(booking,
|
||||
repository.planByBooking(booking.getId()), values).stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
|
||||
@@ -442,6 +447,11 @@ public class SmartLoadingService {
|
||||
@BusinessAudit(event = "SMART_LOADING_PALLET_ASSIGN", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "按已封托托盘分配智能装柜明细")
|
||||
public Plan assignPallet(long planId, AssignPalletRequest request, long operatorId, String operatorName) {
|
||||
return assignPallet(planId, request, operatorId, operatorName, true);
|
||||
}
|
||||
|
||||
private Plan assignPallet(long planId, AssignPalletRequest request,
|
||||
long operatorId, String operatorName, boolean recalculateFees) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("ASSIGN_PALLET|" + planId + '|' + request.palletId() + '|'
|
||||
+ request.expectedVersion());
|
||||
@@ -475,7 +485,10 @@ public class SmartLoadingService {
|
||||
json(values("palletId", request.palletId(), "version", request.expectedVersion())),
|
||||
json(values("requestHash", requestHash, "palletNo", first.getPalletNo(), "lineCount", lines.size())),
|
||||
operatorId, operatorName);
|
||||
recalculateAfterRelationshipChange(assigned.getContainerBookingId(), Set.of(first.getAccountId()), operation);
|
||||
if (recalculateFees) {
|
||||
recalculateAfterRelationshipChange(assigned.getContainerBookingId(),
|
||||
Set.of(first.getAccountId()), operation);
|
||||
}
|
||||
return view(assigned, requireBooking(assigned.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@@ -901,14 +914,18 @@ public class SmartLoadingService {
|
||||
resourceId = "#planId", description = "执行智能装柜推荐")
|
||||
public Plan recommend(long planId, RecommendRequest request, long operatorId, String operatorName) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("RECOMMEND|" + planId + '|' + request.expectedVersion());
|
||||
String scopedInboundCode = trim(request.inboundCode());
|
||||
String requestHash = sha256("RECOMMEND|" + planId + '|' + request.expectedVersion()
|
||||
+ '|' + request.customerId() + '|' + scopedInboundCode);
|
||||
Plan replay = replay(operation, planId, "SMART_RECOMMEND", null, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
Set<Long> visitedGroups = new HashSet<>();
|
||||
Set<Long> visitedPallets = new HashSet<>();
|
||||
for (InboundCandidate candidate : recommendationCandidates(booking)) {
|
||||
Set<Long> affectedCustomers = new HashSet<>();
|
||||
for (InboundCandidate candidate : recommendationCandidates(booking,
|
||||
request.customerId(), scopedInboundCode)) {
|
||||
if (candidate.coLoadGroupId() != null) {
|
||||
if (!visitedGroups.add(candidate.coLoadGroupId()) || StringUtils.hasText(candidate.blockReason())) {
|
||||
continue;
|
||||
@@ -943,7 +960,8 @@ public class SmartLoadingService {
|
||||
plan = repository.plan(planId);
|
||||
assignPallet(planId, new AssignPalletRequest(candidate.palletId(), plan.getVersion(),
|
||||
"REC-PAL-" + sha256(operation + '|' + candidate.palletId()).substring(0, 48)),
|
||||
operatorId, operatorName);
|
||||
operatorId, operatorName, false);
|
||||
affectedCustomers.add(candidate.accountId());
|
||||
continue;
|
||||
}
|
||||
int count = fittingBoxes(booking, repository.items(planId), candidate.availablePackageCount(),
|
||||
@@ -952,12 +970,15 @@ public class SmartLoadingService {
|
||||
plan = repository.plan(planId);
|
||||
assignInbound(planId, new AssignInboundRequest(candidate.inboundId(), count,
|
||||
plan.getVersion(), "REC-INB-" + sha256(operation + '|' + candidate.inboundId())
|
||||
.substring(0, 48)), operatorId, operatorName);
|
||||
.substring(0, 48)), operatorId, operatorName, false, null, false);
|
||||
affectedCustomers.add(candidate.accountId());
|
||||
}
|
||||
}
|
||||
recalculateAfterRelationshipChange(booking.getId(), affectedCustomers, operation);
|
||||
LoadingPlanEntity recommended = repository.plan(planId);
|
||||
history(planId, null, "SMART_RECOMMEND", plan.getPlanStatus(), recommended.getPlanStatus(), operation,
|
||||
json(values("version", request.expectedVersion())),
|
||||
json(values("version", request.expectedVersion(), "customerId", request.customerId(),
|
||||
"inboundCode", scopedInboundCode)),
|
||||
json(values("requestHash", requestHash, "version", recommended.getVersion())),
|
||||
operatorId, operatorName);
|
||||
return view(recommended, requireBooking(plan.getContainerBookingId()));
|
||||
|
||||
@@ -678,6 +678,20 @@ paths:
|
||||
responses:
|
||||
'200': {description: 成功、计划不存在或计划已进入预装流程}
|
||||
'403': {description: 无智能装柜查询权限}
|
||||
/api/platform/v1/first-leg/smart-loading/plans/{planId}/recommend:
|
||||
post:
|
||||
summary: 按当前筛选范围执行智能装柜推荐
|
||||
description: >-
|
||||
需要 platform:firstleg:smart-loading:assign 权限。customerId 与 inboundCode 均为可选推荐范围;
|
||||
传入时只推荐同时命中的候选,避免把同渠道历史候选批量加入当前柜。未传时按柜次渠道推荐全部可用候选。
|
||||
一件代发仍按每个已封托托盘整体加入,不得按箱拆分;同一次推荐完成后按受影响客户统一重算费用。
|
||||
parameters:
|
||||
- {name: planId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
requestBody:
|
||||
required: true
|
||||
content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingRecommendRequest'}}}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,data 为最新 SmartLoadingPlan}
|
||||
/api/platform/v1/first-leg/smart-loading/plans/{planId}/assignments:
|
||||
post:
|
||||
summary: 向预装计划新增入库单明细
|
||||
@@ -2304,6 +2318,14 @@ components:
|
||||
packageCount: {type: integer, minimum: 1, description: 待审核订单本次占用的申报箱数}
|
||||
expectedVersion: {type: integer, minimum: 0, description: 当前 planVersion}
|
||||
operationId: {type: string, minLength: 1, maxLength: 128}
|
||||
SmartLoadingRecommendRequest:
|
||||
type: object
|
||||
required: [expectedVersion, operationId]
|
||||
properties:
|
||||
expectedVersion: {type: integer, minimum: 0, description: 当前 planVersion}
|
||||
operationId: {type: string, minLength: 1, maxLength: 128}
|
||||
customerId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: 当前页面客户筛选;传入时限制推荐范围}
|
||||
inboundCode: {type: string, minLength: 1, maxLength: 64, nullable: true, description: 当前页面入仓码筛选;传入时限制推荐范围}
|
||||
SmartLoadingAdjustRequest:
|
||||
type: object
|
||||
required: [packageCount, expectedPlanVersion, expectedItemVersion, operationId]
|
||||
|
||||
@@ -764,7 +764,7 @@ class SmartLoadingServiceTest {
|
||||
when(repository.candidates(null)).thenReturn(List.of(first, second, third));
|
||||
when(coloadGroups.findByInbound(anyLong(), anyLong())).thenReturn(group);
|
||||
|
||||
Plan result = service.recommend(1L, new RecommendRequest(0, "recommend-group-too-large"), 7L, "u");
|
||||
Plan result = service.recommend(1L, new RecommendRequest(0, "recommend-group-too-large", null, null), 7L, "u");
|
||||
|
||||
assertThat(result.items()).isEmpty();
|
||||
verify(coloadGroups, never()).lockWholeGroupForAssignment(anyLong(), anyLong(), anyLong(),
|
||||
@@ -775,6 +775,7 @@ class SmartLoadingServiceTest {
|
||||
@Test
|
||||
void recommendAssignsMultipleDropshipCandidatesThroughWholePalletCommands() {
|
||||
Candidate dropship = candidate(9L, 10L, 20L, 30L, 20);
|
||||
Candidate anotherCustomer = candidate(19L, 11L, 21L, 31L, 10);
|
||||
AtomicInteger version = new AtomicInteger();
|
||||
List<LoadingPlanItemEntity> saved = new ArrayList<>();
|
||||
InboundOrderEntity inbound = inbound(9L, 10L);
|
||||
@@ -783,7 +784,7 @@ class SmartLoadingServiceTest {
|
||||
when(repository.planByBooking(99L)).thenAnswer(invocation -> plan("EDITING", version.get()));
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.items(1L)).thenAnswer(invocation -> List.copyOf(saved));
|
||||
when(repository.candidates(null)).thenReturn(List.of(dropship));
|
||||
when(repository.candidates(null)).thenReturn(List.of(dropship, anotherCustomer));
|
||||
when(repository.inbound(9L, 10L)).thenReturn(inbound);
|
||||
when(repository.activeItemsByPallet(anyLong())).thenReturn(List.of());
|
||||
when(repository.activePalletItem(eq(1L), anyLong(), eq(20L))).thenReturn(null);
|
||||
@@ -810,7 +811,7 @@ class SmartLoadingServiceTest {
|
||||
}).when(repository).insert(any(LoadingPlanItemEntity.class));
|
||||
|
||||
Plan result = service.recommend(1L,
|
||||
new RecommendRequest(0, "recommend-dropship-pallet"), 7L, "u");
|
||||
new RecommendRequest(0, "recommend-dropship-pallet", 9L, "I10"), 7L, "u");
|
||||
|
||||
assertThat(result.version()).isEqualTo(2);
|
||||
assertThat(saved).hasSize(2).allSatisfy(item -> {
|
||||
@@ -823,6 +824,9 @@ class SmartLoadingServiceTest {
|
||||
org.assertj.core.groups.Tuple.tuple(801L, "DP-260914-0021"),
|
||||
org.assertj.core.groups.Tuple.tuple(802L, "DP-260914-0022"));
|
||||
verify(repository, never()).candidate(anyLong(), anyLong(), anyLong());
|
||||
verify(dropshipPallets, never()).palletCandidates(11L);
|
||||
verify(fbaPricingService, times(1)).recalculateUnlockedContainerCustomerFees(
|
||||
eq(99L), eq(9L), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -839,7 +843,7 @@ class SmartLoadingServiceTest {
|
||||
when(dropshipPallets.sealedPalletLines(801L)).thenReturn(List.of());
|
||||
|
||||
Plan result = service.recommend(1L,
|
||||
new RecommendRequest(0, "recommend-stale-pallet"), 7L, "u");
|
||||
new RecommendRequest(0, "recommend-stale-pallet", null, null), 7L, "u");
|
||||
|
||||
assertThat(result.items()).isEmpty();
|
||||
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString());
|
||||
@@ -888,7 +892,7 @@ class SmartLoadingServiceTest {
|
||||
when(coloadGroups.findByInbound(9L, 12L)).thenReturn(group);
|
||||
|
||||
Plan result = service.recommend(1L,
|
||||
new RecommendRequest(0, "recommend-refresh-group-capacity"), 7L, "u");
|
||||
new RecommendRequest(0, "recommend-refresh-group-capacity", null, null), 7L, "u");
|
||||
|
||||
assertThat(result.version()).isEqualTo(2);
|
||||
assertThat(saved).extracting(LoadingPlanItemEntity::getInboundId,
|
||||
@@ -1397,9 +1401,9 @@ class SmartLoadingServiceTest {
|
||||
|
||||
@Test
|
||||
void replaysRecommendation() {
|
||||
RecommendRequest request = new RecommendRequest(0, "recommend-replay");
|
||||
RecommendRequest request = new RecommendRequest(0, "recommend-replay", null, null);
|
||||
when(repository.operation("recommend-replay")).thenReturn(history(1, null,
|
||||
"SMART_RECOMMEND", sha256("RECOMMEND|1|0")));
|
||||
"SMART_RECOMMEND", sha256("RECOMMEND|1|0|null|null")));
|
||||
when(repository.plan(1)).thenReturn(plan("LOADING", 8));
|
||||
when(repository.booking(99)).thenReturn(booking());
|
||||
when(repository.items(1)).thenReturn(List.of());
|
||||
|
||||
Reference in New Issue
Block a user