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
@@ -42,9 +42,11 @@ import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
import com.baiyue.lastleg.dropship.model.DropshipOutboundCandidate;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonSkuMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
@@ -97,6 +99,7 @@ public class DeliveryTransferService {
@Resource private WarehouseTransferPalletMapper warehouseTransferPallets;
@Resource private DropshipOrderProfileMapper dropshipProfiles;
@Resource private DropshipCartonMapper dropshipCartons;
@Resource private DropshipCartonSkuMapper dropshipCartonSkus;
@Resource private DropshipSkuInventoryMapper dropshipSkuInventory;
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private InboundPlanPalletAllocationMapper planAllocations;
@@ -396,6 +399,7 @@ public class DeliveryTransferService {
String warehouseId = null;
Map<String, Long> requestedPackages = new LinkedHashMap<>();
Map<String, DropshipOutboundCandidate> candidates = new HashMap<>();
Set<String> requestedMarks = new HashSet<>();
for (DeliveryRequests.OneClickItem input : items) {
String sku = input.sku().trim();
DropshipOutboundCandidate candidate = dropshipProfiles.lockOutboundCandidate(customerId,
@@ -414,8 +418,11 @@ public class DeliveryTransferService {
if (input.cartonMarks().size() != input.packages() || new HashSet<>(input.cartonMarks()).size() != input.cartonMarks().size()) {
throw conflict("Specified carton marks must be unique and equal the package count");
}
if (input.cartonMarks().stream().map(String::trim).anyMatch(mark -> !requestedMarks.add(mark))) {
throw conflict("同一物理箱不能在一张出库单中重复选择");
}
List<DropshipCarton> selected = dropshipCartons.listProfile(customerId, candidate.getProfileId()).stream()
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && input.sku().trim().equals(carton.getSkuCode())
.filter(carton -> "AVAILABLE".equals(carton.getStatus())
&& input.cartonMarks().contains(carton.getCartonMark())).toList();
if (selected.size() != input.cartonMarks().size()) throw conflict("Specified carton marks are unavailable");
}
@@ -426,8 +433,8 @@ public class DeliveryTransferService {
if (requested.getValue() > value(candidate.getAvailablePackages())) {
throw conflict("一件代发可用箱数不足:" + candidate.getPalletNo() + " / " + sku);
}
List<DropshipCarton> cartons = dropshipCartons.lockAvailable(customerId,
candidate.getProfileId(), sku, requested.getValue());
List<DropshipCarton> cartons = dropshipCartons.lockAvailablePhysical(customerId,
candidate.getProfileId(), requested.getValue());
if (cartons.size() != requested.getValue()) {
throw conflict("一件代发可用箱数不足:" + candidate.getPalletNo() + " / " + sku);
}
@@ -560,19 +567,31 @@ public class DeliveryTransferService {
List<DropshipCarton> cartons = !selectedMarks.isEmpty()
? selectedMarks.stream()
.map(mark -> dropshipCartons.findByProfileAndMark(customerId, profile.getId(), mark)).filter(Objects::nonNull)
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && item.getSkuCode().equals(carton.getSkuCode())).toList()
: dropshipCartons.lockAvailable(customerId, profile.getId(), item.getSkuCode(), request.packages());
.filter(carton -> "AVAILABLE".equals(carton.getStatus())).toList()
: dropshipCartons.lockAvailablePhysical(customerId, profile.getId(), request.packages());
if (cartons.size() != request.packages()) throw conflict("Insufficient available one-click dropship cartons");
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(request.inventoryPositionId(), request.packages(), outbound.getOutboundNo(), outbound.getOutboundNo(), null, request.idempotencyKey() + ":ALLOCATE", null);
inventoryCommands.allocate(customerId, operatorId, move);
inventoryCommands.pick(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(), request.packages(), outbound.getOutboundNo(), outbound.getOutboundNo(), null, request.idempotencyKey() + ":PICK", null));
inventoryCommands.ship(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(), request.packages(), outbound.getOutboundNo(), outbound.getOutboundNo(), null, request.idempotencyKey() + ":SHIP", null));
long productQuantity = 0;
Map<String, SkuShipment> shippedSkus = new LinkedHashMap<>();
for (DropshipCarton carton : cartons) {
if (carton.getProductQuantity() == null || carton.getProductQuantity() <= 0) throw conflict("出库箱唛缺少逐箱实收件数");
productQuantity = Math.addExact(productQuantity, carton.getProductQuantity());
List<DropshipCartonSku> cartonFacts = dropshipCartonSkus.listCarton(customerId, carton.getId());
if (cartonFacts.isEmpty()) {
shippedSkus.merge(carton.getSkuCode(), new SkuShipment(1, carton.getProductQuantity()), SkuShipment::add);
} else {
for (DropshipCartonSku fact : cartonFacts) {
shippedSkus.merge(fact.getSkuCode(), new SkuShipment(1, value(fact.getProductQuantity())), SkuShipment::add);
}
}
}
for (Map.Entry<String, SkuShipment> shipment : shippedSkus.entrySet()) {
if (dropshipSkuInventory.deduct(customerId, profile.getId(), shipment.getKey(),
shipment.getValue().packages(), shipment.getValue().quantity()) != 1) {
throw conflict("Insufficient one-click dropship SKU inventory: " + shipment.getKey());
}
}
if (dropshipSkuInventory.deduct(customerId, profile.getId(), item.getSkuCode(), request.packages(), productQuantity) != 1) throw conflict("Insufficient one-click dropship SKU inventory");
for (DropshipCarton carton : cartons) { carton.setStatus("SHIPPED"); carton.setOutboundOrderNo(outbound.getOutboundNo()); carton.setVersion((carton.getVersion() == null ? 0 : carton.getVersion()) + 1); carton.setUpdatedAt(LocalDateTime.now()); dropshipCartons.updateById(carton); }
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(request.inventoryPositionId()); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
item.setPickedPackageCount(value(item.getPickedPackageCount()) + request.packages().intValue()); item.setStatus(item.getPickedPackageCount() >= item.getPackageCount() ? "PICKED" : "PARTIAL_PICKED"); outboundPallets.updateById(item);
@@ -778,7 +797,6 @@ public class DeliveryTransferService {
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId());
List<String> marks = profile == null ? List.of() : dropshipCartons.listProfile(customerId, profile.getId())
.stream().filter(carton -> "AVAILABLE".equals(carton.getStatus()))
.filter(carton -> Objects.equals(item.getSkuCode(), carton.getSkuCode()))
.filter(carton -> !StringUtils.hasText(item.getCartonMarks()) || frozenMarks.contains(carton.getCartonMark()))
.map(DropshipCarton::getCartonMark).filter(StringUtils::hasText).distinct().limit(available).toList();
if (profile != null && marks.isEmpty()) continue;
@@ -983,6 +1001,12 @@ public class DeliveryTransferService {
}
private record PalletPickPlan(Long itemId, Long positionId, long packages,
List<String> cartonMarks, int sequence) {}
private record SkuShipment(long packages, long quantity) {
private SkuShipment add(SkuShipment other) {
return new SkuShipment(Math.addExact(packages, other.packages),
Math.addExact(quantity, other.quantity));
}
}
private BigDecimal totalWeight(String customerId, List<DispatchItem> items) {
List<Long> palletIds = items.stream().map(DispatchItem::getWarehousePalletId)
.filter(Objects::nonNull).distinct().toList();
@@ -17,8 +17,9 @@ public interface CargoDiscardInventoryMapper {
"COUNT(c.id) available_packages FROM ll_order o " +
"JOIN ll_dropship_order_profile p ON p.customer_id=o.customer_id AND p.order_id=o.id " +
"JOIN ll_dropship_sku_inventory s ON s.customer_id=o.customer_id AND s.order_id=o.id AND s.profile_id=p.id " +
"LEFT JOIN ll_dropship_carton c ON c.customer_id=o.customer_id AND c.order_id=o.id " +
"AND c.profile_id=p.id AND c.sku_code=s.sku_code AND c.status='AVAILABLE' " +
"JOIN ll_dropship_carton c ON c.customer_id=o.customer_id AND c.order_id=o.id " +
"AND c.profile_id=p.id AND c.status='AVAILABLE' " +
"JOIN ll_dropship_carton_sku cs ON cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=s.sku_code " +
"WHERE o.customer_id=#{customerId} AND o.order_type='ONE_CLICK_DROPSHIP' " +
"<if test='inboundCode != null and inboundCode != \"\"'>AND o.inbound_code LIKE CONCAT('%',#{inboundCode},'%') </if>" +
"GROUP BY o.id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id,o.overseas_warehouse_name,o.created_at," +
@@ -26,21 +27,28 @@ public interface CargoDiscardInventoryMapper {
List<CargoDiscardCandidateRow> candidates(@Param("customerId") String customerId,
@Param("inboundCode") String inboundCode);
@Select("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
"AND sku_code=#{skuCode} AND status='AVAILABLE'")
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
"AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode})")
long countAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
@Param("skuCode") String skuCode);
@Select("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
"AND profile_id=#{profileId} AND sku_code=#{skuCode} AND status='AVAILABLE'")
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
"AND c.profile_id=#{profileId} AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode})")
long countAvailableProfile(@Param("customerId") String customerId, @Param("orderId") Long orderId,
@Param("profileId") Long profileId, @Param("skuCode") String skuCode);
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
"AND sku_code=#{skuCode} AND status='AVAILABLE' ORDER BY profile_id,carton_sequence,id LIMIT #{limit} FOR UPDATE")
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
"AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode}) " +
"ORDER BY c.profile_id,c.carton_sequence,c.id LIMIT #{limit} FOR UPDATE")
List<DropshipCarton> lockAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
@Param("skuCode") String skuCode, @Param("limit") int limit);
@Select("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} AND status='AVAILABLE'")
long countAvailableOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
@Update("UPDATE ll_dropship_carton SET status=#{toStatus},version=version+1,updated_at=NOW(3) " +
"WHERE customer_id=#{customerId} AND id=#{cartonId} AND status=#{fromStatus}")
int changeCartonStatus(@Param("customerId") String customerId, @Param("cartonId") Long cartonId,
@@ -7,9 +7,12 @@ import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonSkuMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import jakarta.annotation.Resource;
@@ -21,6 +24,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
/** Final inventory mutation for a warehouse-approved cargo discard. */
@@ -28,6 +32,8 @@ import java.util.Map;
public class CargoDiscardInventoryAdjustmentService {
@Resource private CargoDiscardInventoryMapper inventory;
@Resource private DropshipCartonLedgerMapper cartonLedgers;
@Resource private DropshipCartonSkuMapper cartonSkus;
@Resource private DropshipSkuInventoryMapper skuInventories;
@Resource private DropshipOrderProfileMapper profiles;
@Resource private InventoryPositionMapper positions;
@Resource private InventoryBalanceMapper balances;
@@ -40,8 +46,25 @@ public class CargoDiscardInventoryAdjustmentService {
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode());
long before = value(sku.getPackageCount());
if (before < actualPackages || inventory.deductSkuInventory(customerId, sku.getId(), actualPackages) != 1) {
throw conflict("托盘 SKU 库存不足: " + item.getSkuCode());
Map<String, SkuDiscard> discardedSkus = new LinkedHashMap<>();
for (int index = 0; index < actualPackages; index++) {
CargoDiscardCarton link = links.get(index);
DropshipCarton carton = inventory.lockCarton(customerId, link.getCartonId());
if (carton == null) throw conflict("托盘预留箱不存在");
List<DropshipCartonSku> facts = cartonSkus.listCarton(customerId, carton.getId());
if (facts.isEmpty()) {
discardedSkus.merge(carton.getSkuCode(), new SkuDiscard(1, value(carton.getProductQuantity())), SkuDiscard::add);
} else {
for (DropshipCartonSku fact : facts) {
discardedSkus.merge(fact.getSkuCode(), new SkuDiscard(1, value(fact.getProductQuantity())), SkuDiscard::add);
}
}
}
for (Map.Entry<String, SkuDiscard> discarded : discardedSkus.entrySet()) {
if (skuInventories.deduct(customerId, item.getProfileId(), discarded.getKey(),
discarded.getValue().packages(), discarded.getValue().quantity()) != 1) {
throw conflict("托盘 SKU 库存不足: " + discarded.getKey());
}
}
var profile = profiles.selectById(item.getProfileId());
var position = profile == null || profile.getInventoryPositionId() == null ? null
@@ -120,6 +143,11 @@ public class CargoDiscardInventoryAdjustmentService {
}
private static long value(Long value) { return value == null ? 0L : value; }
private record SkuDiscard(long packages, long quantity) {
private SkuDiscard add(SkuDiscard other) {
return new SkuDiscard(Math.addExact(packages, other.packages), Math.addExact(quantity, other.quantity));
}
}
private static String remarkSuffix(String value) {
return value == null || value.isBlank() ? "" : "; " + value.trim();
}
@@ -75,7 +75,9 @@ public class CargoDiscardService {
CandidateAccumulator value = grouped.computeIfAbsent(row.getOrderId(), ignored -> new CandidateAccumulator(row));
value.add(row);
}
return grouped.values().stream().map(CandidateAccumulator::response).toList();
return grouped.values().stream()
.map(value -> value.response(inventory.countAvailableOrder(customerId, value.first.getOrderId())))
.toList();
}
@Transactional
@@ -396,12 +398,11 @@ public class CargoDiscardService {
skus.computeIfAbsent(row.getSkuCode(), ignored -> new CandidateSkuAccumulator(row.getSkuCode(), row.getProductName()))
.packages += row.getAvailablePackages() == null ? 0 : row.getAvailablePackages();
}
private CargoDiscardResponses.Candidate response() {
private CargoDiscardResponses.Candidate response(long physicalPackages) {
List<CargoDiscardResponses.CandidateSku> skuViews = skus.values().stream()
.map(value -> new CargoDiscardResponses.CandidateSku(value.skuCode, value.productName, value.packages)).toList();
long packages = skuViews.stream().mapToLong(CargoDiscardResponses.CandidateSku::availablePackages).sum();
return new CargoDiscardResponses.Candidate(first.getOrderId(), first.getInboundCode(), first.getInboundOrderNo(),
first.getOverseasWarehouseId(), first.getOverseasWarehouseName(), first.getCreatedAt(), packages, skuViews);
first.getOverseasWarehouseId(), first.getOverseasWarehouseName(), first.getCreatedAt(), physicalPackages, skuViews);
}
}
private static final class CandidateSkuAccumulator {
@@ -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);
@@ -120,12 +120,14 @@ public class InboundStockReceiptService {
}
DropshipPalletSnapshot sourcePallet = dropshipPallets.get(pallet.getSourcePalletNo());
if (sourcePallet == null || sourcePallet.packageCount() != value(allocation.getPickedPackageCount())) {
throw conflict("One-click dropship source pallet SKU package total does not match PDA receipt");
}
for (DropshipPalletSnapshot.Sku sku : sourcePallet.skus()) {
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
new SkuSnapshot(sku.skuCode(), sku.productName(), null, sku.packageCount()), receivedAt));
throw conflict("One-click dropship source pallet physical carton total does not match PDA receipt");
}
DropshipPalletSnapshot.Sku single = sourcePallet.skus().size() == 1
? sourcePallet.skus().get(0) : null;
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
new SkuSnapshot(single == null ? "MIXED" : single.skuCode(),
single == null ? "混合SKU" : single.productName(), null,
sourcePallet.packageCount()), receivedAt));
} else {
long allocationPackages = value(allocation.getPickedPackageCount());
for (SkuSnapshot sku : consume(skuRemainders, allocationPackages)) {
@@ -219,9 +219,22 @@ public class LoadingCompletedConsumer {
FirstLegLoadingPlanCompletedV1.Order order = payload.orders().get(index);
Set<String> orderCartonMarks = new HashSet<>();
if ("ONE_CLICK_DROPSHIP".equals(order.orderType())) {
for (FirstLegLoadingPlanCompletedV1.PalletSnapshot pallet : order.pallets()) {
JsonNode rawPallets = rawPayload.path("orders").get(index).path("pallets");
for (int palletIndex = 0; palletIndex < order.pallets().size(); palletIndex++) {
FirstLegLoadingPlanCompletedV1.PalletSnapshot pallet = order.pallets().get(palletIndex);
Set<String> palletCartonMarks = new HashSet<>();
for (FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot sku : pallet.skuPackages()) {
requireSkuCartonMapping(sku.skuCode(), sku.packageCount(), sku.cartonMarks(), orderCartonMarks);
requireSkuCartonMapping(sku.skuCode(), sku.packageCount(), sku.cartonMarks(), new HashSet<>());
palletCartonMarks.addAll(sku.cartonMarks());
}
int manifestPackages = rawPallets.path(palletIndex).path("manifestPackageCount").asInt(-1);
if (palletCartonMarks.size() != manifestPackages) {
throw new IllegalArgumentException("一件代发托盘物理箱数与清单箱数不一致: " + pallet.palletNo());
}
for (String mark : palletCartonMarks) {
if (!orderCartonMarks.add(mark)) {
throw new IllegalArgumentException("一件代发物理箱不能跨托盘重复: " + mark);
}
}
}
dropshipReceivingLabels(order, rawPayload.path("orders").get(index));