feat(phase3): connect PDA settlement to real child orders

This commit is contained in:
2026-08-10 13:02:51 +08:00
parent 20996b050b
commit 253c0e7472
14 changed files with 475 additions and 27 deletions
@@ -0,0 +1,34 @@
package com.baiyue.firstleg.client;
import com.baiyue.common.core.api.ApiResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import java.util.List;
/** Order-domain child-order contract used by the transactional PDA settlement. */
@FeignClient(name = "order-service", contextId = "firstlegOrderFulfillmentSplitClient",
url = "${baiyue.clients.order.url:}", path = "/internal/order/v1/fulfillment-splits")
public interface OrderFulfillmentSplitClient {
@PostMapping
ApiResponse<Response> split(@RequestHeader("X-Internal-Service-Token") String token,
@RequestHeader("X-Operator-Id") long operatorId,
@RequestBody CreateRequest request);
record CreateRequest(Long tenantId, Long parentOrderId, Long sourceOrderId,
Long loadingPlanId, Long containerBookingId,
Integer sourcePackageCount, Integer loadedPackageCount,
Integer remainingPackageCount, Integer expectedParentVersion,
String idempotencyKey, String operationId,
List<QuantityLine> lines, String operatorName) {}
record QuantityLine(Long sourceDetailId, Integer packageCount) {}
record Response(Long parentOrderId, String parentOrderNo, Integer parentVersion,
Child loadedChild, Child remainderChild, Integer sourcePackageCount,
Integer loadedPackageCount, Integer remainingPackageCount,
Long loadingPlanId, Long containerBookingId, String idempotencyKey) {}
record Child(Long orderId, String orderNo, Integer splitSequence, String splitRole,
Integer packageCount, Long sourceOrderId, Long loadingPlanId,
Long containerBookingId) {}
}
@@ -23,7 +23,8 @@ public interface InboundSplitSettlementMapper {
@Select("SELECT container_booking_id FROM fl_loading_plan WHERE id=#{loadingPlanId}")
Long containerBookingId(@Param("loadingPlanId") long loadingPlanId);
@Select("""
SELECT id, tenant_id AS tenantId, inbound_code AS inboundCode, inbound_role AS inboundRole,
SELECT id, tenant_id AS tenantId, order_id AS orderId, order_no AS orderNo,
inbound_code AS inboundCode, inbound_role AS inboundRole,
root_inbound_id AS rootInboundId,
process_status AS processStatus, total_package_count AS totalPackageCount,
package_count AS packageCount, version
@@ -44,7 +45,7 @@ public interface InboundSplitSettlementMapper {
actual_net_weight_kg,measure_completed_at,inbounded_at,express_company_code,express_company_name,
express_tracking_no,public_remark,created_by,created_by_name,created_at,updated_by,updated_at,deleted,version,
inbound_role,parent_inbound_id,root_inbound_id,split_sequence,split_status,split_purpose)
SELECT #{childId},tenant_id,task_id,order_id,order_no,#{childInboundCode},
SELECT #{childId},tenant_id,task_id,#{childOrderId},#{childOrderNo},#{childInboundCode},
CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END,order_status_snapshot,
'APPROVED',fee_status,fee_review_status,notification_status,expected_arrival_at,customer_id,customer_name,
company_id,company_name,warehouse_id,warehouse_name,delivery_warehouse_id,delivery_warehouse_name,business_type,
@@ -58,6 +59,7 @@ public interface InboundSplitSettlementMapper {
""")
int cloneChildWithPurpose(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
@Param("rootInboundId") long rootInboundId, @Param("childId") long childId,
@Param("childOrderId") long childOrderId, @Param("childOrderNo") String childOrderNo,
@Param("childInboundCode") String childInboundCode, @Param("packageCount") int packageCount,
@Param("splitSequence") int splitSequence, @Param("splitPurpose") String splitPurpose,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@@ -264,12 +266,14 @@ public interface InboundSplitSettlementMapper {
int normalizeChildMetrics(@Param("accountId") long accountId, @Param("childInboundId") long childInboundId,
@Param("measurementId") long measurementId);
record ParentRow(Long id, Long tenantId, String inboundCode, String inboundRole, Long rootInboundId,
record ParentRow(Long id, Long tenantId, Long orderId, String orderNo,
String inboundCode, String inboundRole, Long rootInboundId,
String processStatus, Integer totalPackageCount, Integer packageCount,
Integer version) {
public ParentRow(Long id, Long tenantId, String inboundCode, String inboundRole,
String processStatus, Integer totalPackageCount, Integer packageCount, Integer version) {
this(id, tenantId, inboundCode, inboundRole, id, processStatus, totalPackageCount, packageCount, version);
this(id, tenantId, 901L, "ORDER-001", inboundCode, inboundRole, id,
processStatus, totalPackageCount, packageCount, version);
}
}
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.firstleg.client.OrderFulfillmentSplitClient;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper;
@@ -41,6 +42,8 @@ public class PdaPickSettlementService {
private InboundPalletAllocationRepository allocations;
@Resource
private WarehousePalletLocationClient warehouse;
@Resource
private OrderFulfillmentSplitClient orderSplits;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
@@ -154,6 +157,9 @@ public class PdaPickSettlementService {
}
if (root == null || !"MASTER".equals(root.inboundRole())) throw conflict("根主入库单不存在");
OrderFulfillmentSplitClient.Response orderSplit = createOrderSplit(parent, root, measurementLines,
pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId, operationId,
"pda-task-" + taskId, operatorId, operatorName);
long loadedChildId = IdWorker.getId();
long remainderChildId = childPackageCount > 0 ? IdWorker.getId() : 0L;
long batchId = IdWorker.getId();
@@ -162,9 +168,11 @@ public class PdaPickSettlementService {
int parentVersion = root.version() == null ? 0 : root.version();
int sequence = splits.nextSplitSequence(parent.tenantId(), rootInboundId);
requireAffected(splits.cloneChildWithPurpose(parent.tenantId(), rootInboundId, rootInboundId, loadedChildId,
orderSplit.loadedChild().orderId(), orderSplit.loadedChild().orderNo(),
childInboundCode(parent.inboundCode(), loadedChildId), pickedPackageCount, sequence, "LOADED", operatorId, operatorName), "实装子入库单");
if (remainderChildId != 0L) {
requireAffected(splits.cloneChildWithPurpose(parent.tenantId(), rootInboundId, rootInboundId, remainderChildId,
orderSplit.remainderChild().orderId(), orderSplit.remainderChild().orderNo(),
childCode, childPackageCount, sequence + 1, "REMAINDER", operatorId, operatorName), "余量子入库单");
}
requireAffected(splits.insertBatchWithChildren(batchId, parent.tenantId(), rootInboundId, operationId, parentInboundId,
@@ -238,6 +246,62 @@ public class PdaPickSettlementService {
return true;
}
private OrderFulfillmentSplitClient.Response createOrderSplit(
ParentRow source, ParentRow root, List<MeasurementSplitLine> lines,
int loadedPackageCount, int remainingPackageCount, long loadingPlanId,
long containerBookingId, String operationId, String idempotencyKey,
long operatorId, String operatorName) {
if (root.orderId() == null || source.orderId() == null) {
throw conflict("入仓单缺少真实订单引用");
}
List<OrderFulfillmentSplitClient.QuantityLine> quantities = lines.stream()
.map(line -> new OrderFulfillmentSplitClient.QuantityLine(
line.item().inboundDetailId(), line.item().packageCount()))
.toList();
OrderFulfillmentSplitClient.CreateRequest request = new OrderFulfillmentSplitClient.CreateRequest(
source.tenantId(), root.orderId(), source.orderId(), loadingPlanId, containerBookingId,
loadedPackageCount + remainingPackageCount, loadedPackageCount, remainingPackageCount,
null, idempotencyKey, operationId, quantities, operatorName);
ApiResponse<OrderFulfillmentSplitClient.Response> response;
try {
response = orderSplits.split(internalToken, operatorId, request);
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务不可用,无法生成真实履约子订单");
}
if (response == null || !response.successful() || response.data() == null) {
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
throw new BizException(code, response == null || response.message() == null
? "订单服务生成真实履约子订单失败" : response.message());
}
OrderFulfillmentSplitClient.Response split = response.data();
boolean remainderMatches = remainingPackageCount == 0 ? split.remainderChild() == null
: validOrderChild(split.remainderChild(), "REMAINDER", source.orderId(), loadingPlanId,
containerBookingId, remainingPackageCount);
if (!Objects.equals(split.parentOrderId(), root.orderId())
|| !Objects.equals(split.sourcePackageCount(), loadedPackageCount + remainingPackageCount)
|| !Objects.equals(split.loadedPackageCount(), loadedPackageCount)
|| !Objects.equals(split.remainingPackageCount(), remainingPackageCount)
|| !Objects.equals(split.loadingPlanId(), loadingPlanId)
|| !Objects.equals(split.containerBookingId(), containerBookingId)
|| !validOrderChild(split.loadedChild(), "LOADED", source.orderId(), loadingPlanId,
containerBookingId, loadedPackageCount)
|| !remainderMatches) {
throw conflict("订单服务返回的父子订单、柜次或数量不一致");
}
return split;
}
private static boolean validOrderChild(OrderFulfillmentSplitClient.Child child, String role,
Long sourceOrderId, long loadingPlanId,
long containerBookingId, int packageCount) {
return child != null && child.orderId() != null && child.orderId() > 0
&& child.orderNo() != null && !child.orderNo().isBlank()
&& role.equals(child.splitRole()) && Objects.equals(child.packageCount(), packageCount)
&& Objects.equals(child.sourceOrderId(), sourceOrderId)
&& Objects.equals(child.loadingPlanId(), loadingPlanId)
&& Objects.equals(child.containerBookingId(), containerBookingId);
}
private static Map<Long, Integer> pickedByInbound(List<LoadingPickItemEntity> taskItems) {
return taskItems.stream().filter(item -> "PICKED".equals(item.getItemStatus())
|| "PARTIAL".equals(item.getItemStatus()))