fix: 按物理箱贯通一件代发履约箱数

This commit is contained in:
2026-09-16 20:49:35 +08:00
parent 8797c0bef9
commit 545ad2083d
34 changed files with 779 additions and 132 deletions
@@ -0,0 +1,23 @@
package com.baiyue.lastleg.dropship.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/** SKU facts inside one physical dropship carton. */
@Data
@TableName("ll_dropship_carton_sku")
public class DropshipCartonSku {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String customerId;
private Long orderId;
private Long profileId;
private Long cartonId;
private String skuCode;
private String productName;
private Long productQuantity;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -27,6 +27,10 @@ public interface DropshipCartonMapper extends BaseMapper<DropshipCarton> {
@Param("cartonMark") String cartonMark);
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND profile_id=#{profileId} AND sku_code=#{sku} AND status='AVAILABLE' ORDER BY carton_sequence LIMIT #{packages} FOR UPDATE")
List<DropshipCarton> lockAvailable(@Param("customerId") String customerId, @Param("profileId") Long profileId, @Param("sku") String sku, @Param("packages") long packages);
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND profile_id=#{profileId} AND status='AVAILABLE' ORDER BY carton_sequence LIMIT #{packages} FOR UPDATE")
List<DropshipCarton> lockAvailablePhysical(@Param("customerId") String customerId,
@Param("profileId") Long profileId,
@Param("packages") long packages);
@Update("UPDATE ll_dropship_carton SET status='AVAILABLE', outbound_order_no=NULL, updated_at=NOW(3) WHERE customer_id=#{customerId} AND outbound_order_no=#{outboundNo} AND status='SHIPPED'")
int restoreOutbound(@Param("customerId") String customerId, @Param("outboundNo") String outboundNo);
}
@@ -0,0 +1,15 @@
package com.baiyue.lastleg.dropship.mapper;
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface DropshipCartonSkuMapper extends BaseMapper<DropshipCartonSku> {
@Select("SELECT * FROM ll_dropship_carton_sku WHERE customer_id=#{customerId} AND carton_id=#{cartonId} ORDER BY id")
List<DropshipCartonSku> listCarton(@Param("customerId") String customerId,
@Param("cartonId") Long cartonId);
}
@@ -10,21 +10,24 @@ import java.util.List;
public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProfile> {
String OUTBOUND_CANDIDATE_SELECT = "SELECT pos.id inventory_position_id,p.id profile_id," +
"p.warehouse_pallet_id,pos.overseas_warehouse_id,b.overseas_warehouse_name," +
"p.pallet_no,b.inbound_code,o.order_no,o.customer_id,o.customer_name,s.sku_code,s.product_name," +
"LEAST(pos.available_packages,s.package_count,(SELECT COUNT(*) FROM ll_dropship_carton c " +
"WHERE c.customer_id=p.customer_id AND c.profile_id=p.id AND c.sku_code=s.sku_code " +
"p.pallet_no,b.inbound_code,o.order_no,o.customer_id,o.customer_name," +
"(SELECT CASE WHEN COUNT(*)=1 THEN MAX(s.sku_code) ELSE 'MIXED' END FROM ll_dropship_sku_inventory s " +
"WHERE s.customer_id=p.customer_id AND s.profile_id=p.id) sku_code," +
"(SELECT CASE WHEN COUNT(*)=1 THEN MAX(s.product_name) ELSE '混合SKU' END FROM ll_dropship_sku_inventory s " +
"WHERE s.customer_id=p.customer_id AND s.profile_id=p.id) product_name," +
"LEAST(pos.available_packages,(SELECT COUNT(*) FROM ll_dropship_carton c " +
"WHERE c.customer_id=p.customer_id AND c.profile_id=p.id " +
"AND c.status='AVAILABLE')) available_packages FROM ll_dropship_order_profile p " +
"JOIN ll_order o ON o.id=p.order_id AND o.customer_id=p.customer_id " +
"AND o.order_type='ONE_CLICK_DROPSHIP' AND COALESCE(o.detained,0)=0 " +
"JOIN ll_inventory_position pos ON pos.id=p.inventory_position_id AND pos.customer_id=p.customer_id " +
"AND pos.pallet_id=p.warehouse_pallet_id " +
"JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " +
"JOIN ll_dropship_sku_inventory s ON s.customer_id=p.customer_id AND s.order_id=p.order_id " +
"AND s.profile_id=p.id ";
"";
String OUTBOUND_CANDIDATE_WHERE = "WHERE (#{customerId} IS NULL OR #{customerId}='' OR p.customer_id=#{customerId}) AND p.status='ACTIVE' " +
"AND pos.status='OCCUPIED' AND pos.available_packages>0 AND s.package_count>0 " +
"AND pos.status='OCCUPIED' AND pos.available_packages>0 " +
"AND EXISTS (SELECT 1 FROM ll_dropship_carton c WHERE c.customer_id=p.customer_id " +
"AND c.profile_id=p.id AND c.sku_code=s.sku_code AND c.status='AVAILABLE') " +
"AND c.profile_id=p.id AND c.status='AVAILABLE') " +
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock dl WHERE dl.customer_id=p.customer_id " +
"AND dl.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id) AND dl.status='ACTIVE' " +
"AND (dl.expires_at IS NULL OR dl.expires_at>CURRENT_TIMESTAMP(3))) " +
@@ -33,7 +36,7 @@ public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProf
"AND NOT EXISTS (SELECT 1 FROM ll_outbound_pallet_item oi JOIN ll_outbound_order oo " +
"ON oo.id=oi.outbound_order_id AND oo.customer_id=oi.customer_id " +
"WHERE oi.customer_id=p.customer_id AND oi.warehouse_pallet_id=p.warehouse_pallet_id " +
"AND oi.inbound_code=b.inbound_code AND oi.sku_code=s.sku_code AND oo.status IN ('WAIT_PICK','PICKING','PICKED')) ";
"AND oi.inbound_code=b.inbound_code AND oo.status IN ('WAIT_PICK','PICKING','PICKED')) ";
@Select("SELECT * FROM ll_dropship_order_profile WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY source_pallet_no")
List<DropshipOrderProfile> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
@@ -50,12 +53,15 @@ public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProf
@Select("<script>" + OUTBOUND_CANDIDATE_SELECT + OUTBOUND_CANDIDATE_WHERE +
"<if test='warehouseId != null and warehouseId != \"\"'>AND pos.overseas_warehouse_id=#{warehouseId} </if>" +
"ORDER BY b.overseas_warehouse_name,p.pallet_no,s.sku_code LIMIT 500</script>")
"ORDER BY b.overseas_warehouse_name,p.pallet_no,sku_code LIMIT 500</script>")
List<DropshipOutboundCandidate> listOutboundCandidates(@Param("customerId") String customerId,
@Param("warehouseId") String warehouseId);
@Select(OUTBOUND_CANDIDATE_SELECT + OUTBOUND_CANDIDATE_WHERE +
"AND pos.id=#{positionId} AND s.sku_code=#{skuCode} LIMIT 1 FOR UPDATE")
"AND pos.id=#{positionId} " +
"AND #{skuCode}=(SELECT CASE WHEN COUNT(*)=1 THEN MAX(s.sku_code) ELSE 'MIXED' END " +
"FROM ll_dropship_sku_inventory s WHERE s.customer_id=p.customer_id AND s.profile_id=p.id) " +
"LIMIT 1 FOR UPDATE")
DropshipOutboundCandidate lockOutboundCandidate(@Param("customerId") String customerId,
@Param("positionId") Long positionId,
@Param("skuCode") String skuCode);
@@ -27,6 +27,7 @@ public class DropshipOrderService {
@Resource private DropshipOrderProfileMapper profiles;
@Resource private DropshipSkuInventoryMapper skuInventory;
@Resource private DropshipCartonMapper cartons;
@Resource private DropshipCartonSkuMapper cartonSkus;
@Resource private DropshipCartonLedgerMapper ledger;
@Transactional
@@ -34,8 +35,9 @@ public class DropshipOrderService {
LastlegOrder order = orders.selectById(request.orderId());
if (order == null || !request.customerId().equals(order.getCustomerId())) throw conflict("Tail order not found");
if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) throw conflict("Order is not a one-click dropship order");
long totalPackages = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
.mapToLong(DropshipRequests.SkuPackages::packageCount).sum();
validateMarks(request);
long totalPackages = request.pallets().stream()
.mapToLong(DropshipRequests.Pallet::manifestPackageCount).sum();
long totalQuantity = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
.mapToLong(DropshipRequests.SkuPackages::productQuantity).sum();
if (order.getPackageCount() != null && order.getPackageCount().longValue() != totalPackages) {
@@ -65,12 +67,11 @@ public class DropshipOrderService {
return Math.toIntExact(totalPackages);
}
validateMarks(request);
LocalDateTime now = LocalDateTime.now();
for (DropshipRequests.Pallet inputPallet : request.pallets()) {
DropshipOrderProfile profile = profile(request, order, inputPallet, now);
profiles.insert(profile);
int sequence = 0;
Map<String, List<CartonSkuFact>> factsByMark = new java.util.LinkedHashMap<>();
for (DropshipRequests.SkuPackages input : inputPallet.skuPackages()) {
DropshipSkuInventory inventory = new DropshipSkuInventory();
inventory.setId(IdWorker.getId()); inventory.setCustomerId(request.customerId()); inventory.setOrderId(order.getId());
@@ -79,26 +80,43 @@ public class DropshipOrderService {
inventory.setCreatedAt(now); inventory.setUpdatedAt(now);
skuInventory.insert(inventory);
for (String mark : input.cartonMarks()) {
DropshipCarton carton = new DropshipCarton();
carton.setId(IdWorker.getId()); carton.setCustomerId(request.customerId()); carton.setOrderId(order.getId());
carton.setProfileId(profile.getId()); carton.setWarehousePalletId(null); carton.setPalletNo(null);
carton.setSkuCode(input.skuCode()); carton.setCartonMark(mark.trim()); carton.setCartonSequence(++sequence);
carton.setProductQuantity(input.cartonQuantities().get(mark));
carton.setStatus("PENDING_INBOUND"); carton.setVersion(0L); carton.setCreatedAt(now); carton.setUpdatedAt(now);
cartons.insert(carton);
ledger.insert(stockInFact(request, order, carton, now));
factsByMark.computeIfAbsent(mark.trim(), ignored -> new java.util.ArrayList<>())
.add(new CartonSkuFact(input.skuCode().trim(), input.productName(),
input.cartonQuantities().get(mark)));
}
}
int sequence = 0;
for (Map.Entry<String, List<CartonSkuFact>> entry : factsByMark.entrySet()) {
List<CartonSkuFact> facts = entry.getValue();
DropshipCarton carton = new DropshipCarton();
carton.setId(IdWorker.getId()); carton.setCustomerId(request.customerId()); carton.setOrderId(order.getId());
carton.setProfileId(profile.getId()); carton.setWarehousePalletId(null); carton.setPalletNo(null);
carton.setSkuCode(facts.size() == 1 ? facts.get(0).skuCode() : "MIXED");
carton.setCartonMark(entry.getKey()); carton.setCartonSequence(++sequence);
carton.setProductQuantity(facts.stream().mapToLong(CartonSkuFact::quantity).sum());
carton.setStatus("PENDING_INBOUND"); carton.setVersion(0L); carton.setCreatedAt(now); carton.setUpdatedAt(now);
cartons.insert(carton);
for (CartonSkuFact fact : facts) {
DropshipCartonSku sku = new DropshipCartonSku();
sku.setId(IdWorker.getId()); sku.setCustomerId(request.customerId()); sku.setOrderId(order.getId());
sku.setProfileId(profile.getId()); sku.setCartonId(carton.getId()); sku.setSkuCode(fact.skuCode());
sku.setProductName(fact.productName()); sku.setProductQuantity(fact.quantity());
sku.setCreatedAt(now); sku.setUpdatedAt(now); cartonSkus.insert(sku);
}
ledger.insert(stockInFact(request, order, carton, now));
}
}
return Math.toIntExact(totalPackages);
}
private void validateMarks(DropshipRequests.Receive request) {
Set<String> marks = new HashSet<>();
Map<String, String> palletByMark = new java.util.HashMap<>();
Set<String> palletNos = new HashSet<>();
for (DropshipRequests.Pallet pallet : request.pallets()) {
if (!palletNos.add(pallet.palletNo().trim())) throw conflict("Duplicate pallet number: " + pallet.palletNo());
String palletNo = pallet.palletNo().trim();
if (!palletNos.add(palletNo)) throw conflict("Duplicate pallet number: " + pallet.palletNo());
Set<String> skus = new HashSet<>();
Set<String> physicalMarks = new HashSet<>();
for (DropshipRequests.SkuPackages item : pallet.skuPackages()) {
if (item.packageCount() != item.cartonMarks().size()) {
throw conflict("SKU packageCount must equal cartonMarks count: " + item.skuCode());
@@ -110,10 +128,20 @@ public class DropshipOrderService {
throw conflict("逐箱实收件数必须完整且与 SKU 汇总件数一致");
}
if (!skus.add(item.skuCode().trim())) throw conflict("Duplicate SKU on pallet: " + item.skuCode());
Set<String> skuMarks = new HashSet<>();
for (String mark : item.cartonMarks()) {
if (!marks.add(mark.trim())) throw conflict("Duplicate carton mark: " + mark);
String normalized = mark.trim();
if (!skuMarks.add(normalized)) throw conflict("Duplicate carton mark for SKU: " + mark);
String existingPallet = palletByMark.putIfAbsent(normalized, palletNo);
if (existingPallet != null && !existingPallet.equals(palletNo)) {
throw conflict("Carton mark belongs to multiple pallets: " + mark);
}
physicalMarks.add(normalized);
}
}
if (physicalMarks.size() != pallet.manifestPackageCount()) {
throw conflict("Pallet physical carton total does not equal manifest package count: " + palletNo);
}
}
}
@@ -143,7 +171,7 @@ public class DropshipOrderService {
value(sku.getPackageCount()), value(sku.getProductQuantity())))
.toList();
return new DropshipPalletSnapshot(profile.getSourcePalletNo(),
skus.stream().mapToLong(DropshipPalletSnapshot.Sku::packageCount).sum(), skus);
value(profile.getManifestPackageCount()), skus);
}).toList();
}
@@ -213,6 +241,9 @@ public class DropshipOrderService {
}
private static long value(Long value) { return value == null ? 0L : value; }
private static long value(Integer value) { return value == null ? 0L : value.longValue(); }
private record CartonSkuFact(String skuCode, String productName, long quantity) {}
private static ResponseStatusException conflict(String message) {
return new ResponseStatusException(HttpStatus.CONFLICT, message);