fix: 按物理箱贯通一件代发履约箱数
This commit is contained in:
+33
-9
@@ -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();
|
||||
|
||||
+16
-8
@@ -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,
|
||||
|
||||
+30
-2
@@ -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();
|
||||
}
|
||||
|
||||
+5
-4
@@ -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 {
|
||||
|
||||
+23
@@ -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;
|
||||
}
|
||||
+4
@@ -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);
|
||||
}
|
||||
|
||||
+15
@@ -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);
|
||||
}
|
||||
+16
-10
@@ -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);
|
||||
|
||||
+47
-16
@@ -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);
|
||||
|
||||
+7
-5
@@ -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)) {
|
||||
|
||||
+15
-2
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user