feat: add dropship fulfillment producer chain

This commit is contained in:
2026-08-25 08:40:27 +08:00
parent c218bfad6b
commit 4839243f21
35 changed files with 389 additions and 53 deletions
@@ -59,6 +59,9 @@ public class InternalInboundOrderController {
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()
|| command.inboundCode() == null || command.inboundCode().isBlank()
|| command.inboundCode().length() > 40
|| command.fulfillmentType() == null
|| (!"STANDARD".equals(command.fulfillmentType())
&& !"ONE_CLICK_DROPSHIP".equals(command.fulfillmentType()))
|| command.totalPackageCount() != null && command.totalPackageCount() < 1) {
throw new BizException(ResultCode.BAD_REQUEST, "订单入库快照不完整");
}
@@ -142,7 +145,10 @@ public class InternalInboundOrderController {
|| command.customerName() == null || command.customerName().isBlank()
|| command.warehouseId() == null || command.warehouseId() <= 0
|| command.warehouseName() == null || command.warehouseName().isBlank()
|| command.businessType() == null || command.businessType().isBlank()) {
|| command.businessType() == null || command.businessType().isBlank()
|| command.fulfillmentType() == null
|| (!"STANDARD".equals(command.fulfillmentType())
&& !"ONE_CLICK_DROPSHIP".equals(command.fulfillmentType()))) {
throw new BizException(ResultCode.BAD_REQUEST, "草稿预入库快照不完整");
}
}
@@ -17,7 +17,7 @@ public record InboundOrderSummary(
Long customerId, String customerName, Long companyId, String companyName,
Long warehouseId, String warehouseName,
Long deliveryWarehouseId, String deliveryWarehouseName,
String businessType, String channelCode, String channelName,
String businessType, String fulfillmentType, String channelCode, String channelName,
String taxMode, String pricingTaxMode, String containerMode, Boolean customsInspection,
String destinationType, Long destinationWarehouseId, String destinationCode, String destinationName,
Boolean holdFlag, String currentHoldReason,
@@ -56,6 +56,7 @@ public record InboundOrderSummary(
order.getCustomerId(), order.getCustomerName(),
order.getCompanyId(), order.getCompanyName(), order.getWarehouseId(), order.getWarehouseName(),
order.getDeliveryWarehouseId(), order.getDeliveryWarehouseName(), order.getBusinessType(),
order.getFulfillmentType(),
order.getChannelCode(), order.getChannelName(), order.getTaxMode(), order.getPricingTaxMode(),
order.getContainerMode(),
order.getCustomsInspection(), order.getDestinationType(), order.getDestinationWarehouseId(),
@@ -33,7 +33,8 @@ public record OrderApprovedInboundCommand(
String expressTrackingNo,
Integer totalPackageCount,
String pickupMethod,
LocalDateTime requestedPickupAt
LocalDateTime requestedPickupAt,
String fulfillmentType
) {
public OrderApprovedInboundCommand(
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
@@ -48,7 +49,7 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, totalPackageCount, null, null);
expressTrackingNo, totalPackageCount, null, null, "STANDARD");
}
/** 兼容尚未携带订单总箱数的历史事件。 */
public OrderApprovedInboundCommand(
@@ -63,7 +64,7 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, null, null, null);
expressTrackingNo, null, null, null, "STANDARD");
}
/** 兼容未携带报价包税维度的历史调用方。 */
@@ -79,6 +80,6 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, null, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, null, null, null);
expressTrackingNo, null, null, null, "STANDARD");
}
}
@@ -38,6 +38,7 @@ public class InboundOrderEntity extends BaseEntity {
private Long deliveryWarehouseId;
private String deliveryWarehouseName;
private String businessType;
private String fulfillmentType;
private String channelCode;
private String channelName;
private Long carrierId;
@@ -38,7 +38,7 @@ public interface InboundSplitSettlementMapper {
INSERT INTO fl_inbound_order (id,tenant_id,task_id,order_id,order_no,inbound_code,process_status,
order_status_snapshot,measure_review_status,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,channel_code,channel_name,carrier_id,carrier_name,
delivery_warehouse_id,delivery_warehouse_name,business_type,fulfillment_type,channel_code,channel_name,carrier_id,carrier_name,
tax_mode,pricing_tax_mode,container_mode,customs_inspection,destination_type,destination_warehouse_id,
destination_code,destination_name,hold_flag,current_hold_reason,total_package_count,package_count,
actual_gross_weight_kg,chargeable_weight_kg,actual_volume_cbm,chargeable_volume_cbm,pallet_count,volume_weight_kg,
@@ -48,7 +48,7 @@ public interface InboundSplitSettlementMapper {
SELECT #{childId},tenant_id,task_id,#{childOrderId},#{childOrderNo},#{childInboundCode},
CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'WAIT_SPLIT' 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,
company_id,company_name,warehouse_id,warehouse_name,delivery_warehouse_id,delivery_warehouse_name,business_type,fulfillment_type,
channel_code,channel_name,carrier_id,carrier_name,tax_mode,pricing_tax_mode,container_mode,customs_inspection,
destination_type,destination_warehouse_id,destination_code,destination_name,hold_flag,current_hold_reason,
#{packageCount},#{packageCount},
@@ -19,7 +19,7 @@ public interface LoadingCompletedEventMapper {
COALESCE(o.root_inbound_id, o.id) AS root_inbound_id,
o.order_id AS source_order_id, o.order_no AS source_order_no,
COALESCE(root_order.order_id, o.order_id) AS root_order_id,
o.inbound_code, o.business_type, o.destination_type, o.destination_warehouse_id, o.destination_code AS destination_warehouse_code,
o.inbound_code, o.business_type, o.fulfillment_type, o.destination_type, o.destination_warehouse_id, o.destination_code AS destination_warehouse_code,
o.destination_name AS destination_warehouse_name,
i.id AS loading_plan_item_id, i.inbound_detail_id AS source_inbound_detail_id, i.sku_code,
i.product_name_zh, i.product_name_en, i.package_count, i.product_quantity,
@@ -73,7 +73,7 @@ public interface LoadingCompletedEventMapper {
private Integer productQuantity; private Integer palletCount; private BigDecimal grossWeightKg;
private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm;
private String fbaTrackingNo; private String shipmentNo; private String shippingMark;
private String businessType; private String destinationType;
private String businessType; private String fulfillmentType; private String destinationType;
private String customerRemark; private String internalRemark; private String warehouseRemark;
}
@@ -68,6 +68,7 @@ public class InboundOrderCreationService {
inbound.setDeliveryWarehouseId(command.deliveryWarehouseId());
inbound.setDeliveryWarehouseName(command.deliveryWarehouseName());
inbound.setBusinessType(command.businessType());
inbound.setFulfillmentType(fulfillmentType(command.fulfillmentType()));
inbound.setChannelCode(command.channelCode());
inbound.setChannelName(command.channelName());
inbound.setCarrierId(command.carrierId());
@@ -160,6 +161,7 @@ public class InboundOrderCreationService {
.set(InboundOrderEntity::getDeliveryWarehouseName, command.deliveryWarehouseName())
.set(InboundOrderEntity::getExpectedArrivalAt, command.expectedArrivalAt())
.set(InboundOrderEntity::getBusinessType, command.businessType())
.set(InboundOrderEntity::getFulfillmentType, fulfillmentType(command.fulfillmentType()))
.set(InboundOrderEntity::getChannelCode, command.channelCode())
.set(InboundOrderEntity::getChannelName, command.channelName())
.set(InboundOrderEntity::getDestinationType, command.destinationType())
@@ -181,6 +183,7 @@ public class InboundOrderCreationService {
.set(InboundOrderEntity::getProcessStatus, "WAIT_MEASURE")
.set(InboundOrderEntity::getOrderStatusSnapshot, "APPROVED")
.set(InboundOrderEntity::getTotalPackageCount, command.totalPackageCount())
.set(InboundOrderEntity::getFulfillmentType, fulfillmentType(command.fulfillmentType()))
.set(InboundOrderEntity::getUpdatedAt, LocalDateTime.now())
.setSql("version = version + 1");
if (orders.update(null, update) != 1) {
@@ -217,6 +220,7 @@ public class InboundOrderCreationService {
inbound.setDeliveryWarehouseId(command.deliveryWarehouseId());
inbound.setDeliveryWarehouseName(command.deliveryWarehouseName());
inbound.setBusinessType(command.businessType());
inbound.setFulfillmentType(fulfillmentType(command.fulfillmentType()));
inbound.setChannelCode(command.channelCode());
inbound.setChannelName(command.channelName());
inbound.setCarrierId(command.carrierId());
@@ -241,6 +245,12 @@ public class InboundOrderCreationService {
return inbound;
}
private static String fulfillmentType(String value) {
if ("STANDARD".equals(value) || "ONE_CLICK_DROPSHIP".equals(value)) return value;
throw new BizException(ResultCode.BAD_REQUEST,
"履约类型必须为 STANDARD 或 ONE_CLICK_DROPSHIP");
}
private void addHistory(long inboundId, long tenantId, String from, String to,
String action, LocalDateTime now) {
InboundStatusHistoryEntity history = new InboundStatusHistoryEntity();
@@ -91,8 +91,9 @@ public class LoadingCompletedOutboxService {
row.getCustomerRemark(), row.getInternalRemark(), row.getWarehouseRemark(), null,
cartonMarks.getOrDefault(row.getLoadingPlanItemId(), List.of())))
.toList();
String orderType = dropship(first.getBusinessType()) ? "ONE_CLICK_DROPSHIP" : "FIRST_LEG";
List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> pallets = dropship(first.getBusinessType())
String fulfillmentType = fulfillmentType(sorted);
String orderType = dropship(fulfillmentType) ? "ONE_CLICK_DROPSHIP" : "FIRST_LEG";
List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> pallets = dropship(fulfillmentType)
? palletSnapshots(sorted, palletRows) : List.of();
if ("ONE_CLICK_DROPSHIP".equals(orderType) && pallets.isEmpty()) {
throw new BizException(ResultCode.CONFLICT, "一键代发入仓单缺少托盘冻结快照");
@@ -110,7 +111,7 @@ public class LoadingCompletedOutboxService {
/** 国内按实装箱数从可用标签池稳定随机抽取箱唛,重复发布同一计划时结果不变。 */
private Map<Long, List<String>> cartonLabelPools(List<Row> rows) {
Map<Long, List<String>> result = new java.util.LinkedHashMap<>();
Map<LabelPoolKey, List<Row>> byInbound = rows.stream().filter(row -> !dropship(row.getBusinessType()))
Map<LabelPoolKey, List<Row>> byInbound = rows.stream().filter(row -> !dropship(row.getFulfillmentType()))
.filter(row -> "OVERSEAS".equals(destinationType(List.of(row))))
.collect(java.util.stream.Collectors.groupingBy(
row -> new LabelPoolKey(row.getAccountId(), row.getSourceInboundId()),
@@ -168,6 +169,18 @@ public class LoadingCompletedOutboxService {
return values.iterator().next();
}
private String fulfillmentType(List<Row> rows) {
Set<String> values = rows.stream().map(Row::getFulfillmentType)
.map(value -> value == null || value.isBlank() ? "STANDARD" : value)
.collect(java.util.stream.Collectors.toSet());
if (values.size() != 1
|| (!values.contains("STANDARD") && !values.contains("ONE_CLICK_DROPSHIP"))) {
throw new BizException(ResultCode.CONFLICT,
"装柜入仓单履约类型必须唯一且为 STANDARD 或 ONE_CLICK_DROPSHIP");
}
return values.iterator().next();
}
private record LabelPoolKey(long accountId, long inboundId) { }
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> palletSnapshots(List<Row> values, List<PalletRow> source) {
@@ -223,8 +236,7 @@ public class LoadingCompletedOutboxService {
}
private static boolean dropship(String value) {
return value != null && ("ONE_CLICK_DROPSHIP".equalsIgnoreCase(value)
|| value.toUpperCase(java.util.Locale.ROOT).contains("DROPSHIP"));
return "ONE_CLICK_DROPSHIP".equals(value);
}
private record DetailSnapshot(Long detailId, String skuCode, String productName,