Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java
This commit is contained in:
2026-09-08 18:37:23 +08:00
5 changed files with 316 additions and 7 deletions
@@ -74,6 +74,8 @@ public class LoadingListService {
@Resource
private LoadingCompletedOutboxService loadingCompletedOutboxService;
@Resource
private FbaPricingService fbaPricingService;
@Resource
private ObjectMapper objectMapper;
@Resource
private LoadingDocumentFileClient documentFiles;
@@ -501,6 +503,7 @@ public class LoadingListService {
AggregateRow before = requirePlan(planId);
List<Link> links = loadingPickService.requirePreloadConfirmationReady(planId);
requireContainerFeesReadyForPreloadConfirmation(links, operationId);
if (repository.touchForPreloadConfirmation(planId, request.expectedVersion(),
operatorId, trim(operatorName)) != 1) {
throw conflict("预装计划已被其他操作修改,请刷新后重试");
@@ -610,6 +613,30 @@ public class LoadingListService {
}
}
/**
* Pre-review/pre-measurement members may be reserved in an editing plan. Confirmation is the
* hard boundary: every customer group in the cabinet must have a ready, freshly recomputed fee.
*/
private void requireContainerFeesReadyForPreloadConfirmation(List<Link> links, String operationId) {
Map<Long, Set<Long>> tenantsByContainer = new LinkedHashMap<>();
for (Link link : links) {
if (link.containerBookingId() == null || link.tenantId() == null) {
throw conflict("预装关联缺少柜次或客户归属,不能确认预装");
}
tenantsByContainer.computeIfAbsent(link.containerBookingId(), ignored -> new java.util.LinkedHashSet<>())
.add(link.tenantId());
}
for (Map.Entry<Long, Set<Long>> entry : tenantsByContainer.entrySet()) {
for (Long tenantId : entry.getValue()) {
var result = fbaPricingService.recalculateUnlockedContainerCustomerFees(entry.getKey(), tenantId,
sha256("CONFIRM_PRELOAD_FEE|" + operationId + '|' + entry.getKey() + '|' + tenantId));
if (result.status() == FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY) {
throw conflict("柜内存在尚未完成初始费用的入仓单,不能确认预装");
}
}
}
}
private static void validateRange(java.time.LocalDateTime from, java.time.LocalDateTime to) {
if (from != null && to != null && from.isAfter(to)) {
throw badRequest("创建开始时间不能晚于结束时间");
@@ -63,6 +63,7 @@ public class SmartLoadingService {
@Resource private LoadingModelBridgeService loadingModelBridge;
@Resource private SplitPreloadService splitPreloadService;
@Resource private ColoadGroupAssignmentPort coloadGroups;
@Resource private FbaPricingService fbaPricingService;
@Resource private PriceBookQueryUseCase priceBooks;
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
@Resource private ObjectMapper objectMapper;
@@ -430,7 +431,13 @@ public class SmartLoadingService {
@BusinessAudit(event = "SMART_LOADING_ITEM_ASSIGN", resourceType = "LOADING_PLAN",
resourceId = "#planId", description = "分配智能装柜明细")
public Plan assign(long planId, AssignRequest request, long operatorId, String operatorName) {
return assign(planId, request, operatorId, operatorName, false, null, null, null, false, null);
return assign(planId, request, operatorId, operatorName, false, null, null, null, false, null, true);
}
private Plan assign(long planId, AssignRequest request, long operatorId, String operatorName,
boolean wholeGroupAssignment) {
return assign(planId, request, operatorId, operatorName, wholeGroupAssignment,
null, null, null, false, null, false);
}
@Transactional
@@ -463,19 +470,21 @@ public class SmartLoadingService {
String childOperation = "PAL-" + sha256(operation + '|' + line.getInboundDetailId()).substring(0, 48);
assign(planId, new AssignRequest(line.getInboundId(), line.getInboundDetailId(), line.getCartonCount(),
current.getVersion(), childOperation), operatorId, operatorName, true, request.palletId(),
line.getPalletNo(), line.getActualSkuQuantity(), sequence++ == 0, candidate);
line.getPalletNo(), line.getActualSkuQuantity(), sequence++ == 0, candidate, false);
}
LoadingPlanEntity assigned = repository.plan(planId);
history(planId, null, "ADD_PALLET", original.getPlanStatus(), assigned.getPlanStatus(), operation,
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);
return view(assigned, requireBooking(assigned.getContainerBookingId()));
}
private Plan assign(long planId, AssignRequest request, long operatorId, String operatorName,
boolean wholeGroupAssignment, Long palletId, String palletNo,
Integer actualSkuQuantity, boolean primaryPalletLine, Candidate frozenCandidate) {
Integer actualSkuQuantity, boolean primaryPalletLine, Candidate frozenCandidate,
boolean recalculateFees) {
String operation = operation(request.operationId());
String requestHash = sha256("ASSIGN|" + planId + '|'
+ request.inboundId() + '|' + request.inboundDetailId() + '|' + request.packageCount()
@@ -544,6 +553,9 @@ public class SmartLoadingService {
loadingModelBridge.bindIncrementalAllocation(planId, candidate.accountId(), candidate.inboundId(),
operatorId, operatorName, operation);
loadingPickService.sync(planId, changed);
if (recalculateFees) {
recalculateAfterRelationshipChange(booking.getId(), Set.of(candidate.accountId()), operation);
}
return view(repository.plan(planId), booking);
}
@@ -557,7 +569,7 @@ public class SmartLoadingService {
resourceId = "#planId", description = "按入仓码分配智能装柜箱数")
public Plan assignInbound(long planId, AssignInboundRequest request,
long operatorId, String operatorName) {
return assignInbound(planId, request, operatorId, operatorName, false);
return assignInbound(planId, request, operatorId, operatorName, false, null, true);
}
/**
@@ -637,17 +649,24 @@ public class SmartLoadingService {
loadingModelBridge.bindIncrementalAllocation(planId, request.customerId(), inboundId,
operatorId, operatorName, operation);
created.forEach(item -> loadingPickService.sync(planId, item));
recalculateAfterRelationshipChange(booking.getId(), Set.of(request.customerId()), operation);
return view(repository.plan(planId), booking);
}
private Plan assignInbound(long planId, AssignInboundRequest request,
long operatorId, String operatorName, boolean wholeGroupAssignment) {
return assignInbound(planId, request, operatorId, operatorName, wholeGroupAssignment, null);
return assignInbound(planId, request, operatorId, operatorName, wholeGroupAssignment, null, false);
}
private Plan assignInbound(long planId, AssignInboundRequest request,
long operatorId, String operatorName, boolean wholeGroupAssignment,
List<Candidate> frozenDetails) {
return assignInbound(planId, request, operatorId, operatorName, wholeGroupAssignment, frozenDetails, false);
}
private Plan assignInbound(long planId, AssignInboundRequest request,
long operatorId, String operatorName, boolean wholeGroupAssignment,
List<Candidate> frozenDetails, boolean recalculateFees) {
String operation = operation(request.operationId());
String requestHash = sha256("ASSIGN_INBOUND|" + planId + '|' + request.inboundId() + '|'
+ request.packageCount() + '|' + request.expectedVersion());
@@ -661,8 +680,13 @@ public class SmartLoadingService {
if (!wholeGroupAssignment && projection != null && Set.of("PREPARING", "WAIT_MEASURE",
"MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED", "WAIT_PRICING",
"FEE_CONFIRMING", "INBOUNDING").contains(projection.getProcessStatus())) {
return assignDeclaredInbound(planId, request, projection, operation, requestHash,
Plan assigned = assignDeclaredInbound(planId, request, projection, operation, requestHash,
operatorId, operatorName);
if (recalculateFees) {
recalculateAfterRelationshipChange(requireBooking(repository.plan(planId).getContainerBookingId()).getId(),
Set.of(projection.getTenantId()), operation);
}
return assigned;
}
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
@@ -686,7 +710,7 @@ public class SmartLoadingService {
.substring(0, 48);
assign(planId, new AssignRequest(request.inboundId(), allocation.candidate().inboundDetailId(),
allocation.packageCount(), current.getVersion(), childOperation), operatorId, operatorName,
wholeGroupAssignment, null, null, null, false, null);
wholeGroupAssignment, null, null, null, false, null, false);
sequence++;
}
LoadingPlanEntity assigned = repository.plan(planId);
@@ -695,6 +719,9 @@ public class SmartLoadingService {
json(values("requestHash", requestHash, "packageCount", request.packageCount(),
"specificationLineCount", sequence, "cartonIdentityStatus", "UNVERIFIED")),
operatorId, operatorName);
if (recalculateFees) {
recalculateAfterRelationshipChange(booking.getId(), Set.of(first.accountId()), operation);
}
return view(assigned, booking);
}
@@ -825,6 +852,8 @@ public class SmartLoadingService {
json(values("requestHash", requestHash, "groupId", locked.groupId(),
"memberCount", locked.members().size(), "groupTotalBoxes", locked.fullPackageCount(),
"candidateInboundCount", memberCandidates.size())), operatorId, operatorName);
recalculateAfterRelationshipChange(booking.getId(), locked.members().stream()
.map(Member::tenantId).collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)), operation);
return view(assigned, booking);
}
@@ -864,6 +893,8 @@ public class SmartLoadingService {
json(values("version", request.expectedPlanVersion(), "groupId", groupId)),
json(values("requestHash", requestHash, "groupId", groupId, "deleted", true)),
operatorId, operatorName);
recalculateAfterRelationshipChange(booking.getId(), group.members().stream()
.map(Member::tenantId).collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)), operation);
return view(repository.plan(planId), booking);
}
@@ -1054,6 +1085,7 @@ public class SmartLoadingService {
history(planId, itemId, "REMOVE_ITEM", plan.getPlanStatus(), plan.getPlanStatus(), operation,
json(values("version", request.expectedPlanVersion(), "packageCount", item.getPackageCount())),
json(values("requestHash", requestHash, "deleted", true)), operatorId, operatorName);
recalculateAfterRelationshipChange(plan.getContainerBookingId(), Set.of(item.getAccountId()), operation);
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
}
@@ -1160,6 +1192,10 @@ public class SmartLoadingService {
json(values("version", request.expectedPlanVersion(), "itemCount", items.size())),
json(values("requestHash", requestHash, "removedItemCount", removable.size(),
"removedGroupCount", removableGroupIds.size())), operatorId, operatorName);
if (!removable.isEmpty()) {
recalculateAfterRelationshipChange(booking.getId(), removable.stream().map(LoadingPlanItemEntity::getAccountId)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)), operation);
}
return view(repository.plan(planId), booking);
}
@@ -1662,6 +1698,17 @@ public class SmartLoadingService {
private static void requireSameCustomerScope(ContainerBookingEntity booking, Candidate candidate) {
if (!sameCustomerScope(booking, candidate)) throw forbidden();
}
/**
* A draft may legitimately contain pre-review or pre-measurement members without a fee snapshot.
* The pricing port reports that as NOT_READY, while any sent/customer-confirmed member fails and
* rolls this relation mutation back with the surrounding transaction.
*/
private void recalculateAfterRelationshipChange(long containerBookingId, Set<Long> tenantIds,
String operation) {
tenantIds.stream().filter(Objects::nonNull).filter(tenantId -> tenantId > 0).sorted()
.forEach(tenantId -> fbaPricingService.recalculateUnlockedContainerCustomerFees(
containerBookingId, tenantId, sha256("SMART_LOADING_RELATION|" + operation + '|' + tenantId)));
}
private void touch(LoadingPlanEntity p,long op,String name){if(repository.touch(p.getId(),p.getVersion(),op,name)!=1)throw conflict("计划已被并发修改");}
private Plan replay(String operation, long planId, String action, Long itemId, String requestHash) {
LoadingPlanHistoryEntity history = repository.operation(operation);