fix(lastleg): account return carton outbound stock atomically
This commit is contained in:
+5
-3
@@ -109,6 +109,7 @@ public class DeliveryTransferService {
|
||||
@Resource private DropshipCartonSkuMapper dropshipCartonSkus;
|
||||
@Resource private DropshipSkuInventoryMapper dropshipSkuInventory;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService returnStock;
|
||||
@Resource private InboundPlanPalletAllocationMapper planAllocations;
|
||||
@Resource private InboundPlanPalletMapper inboundPlanPallets;
|
||||
@Resource private WarehouseResourceGateway warehouseResources;
|
||||
@@ -672,9 +673,10 @@ public class DeliveryTransferService {
|
||||
ReturnInventoryCarton carton = returnCartons.lockMark(item.getCartonMarks());
|
||||
requireReturnSource(outbound, item, carton);
|
||||
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("退货箱唛不可出库:" + item.getCartonMarks());
|
||||
carton.setStatus("OUTBOUNDED"); carton.setUpdatedAt(LocalDateTime.now());
|
||||
if (returnCartons.updateById(carton) != 1) throw conflict("退货箱唛状态已变化,请重新核对");
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(null); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
long outboundPickId = IdWorker.getId();
|
||||
returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(),
|
||||
outbound.getOutboundNo(), outboundPickId);
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(outboundPickId); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(null); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
|
||||
updateOutboundPickingStatus(customerId, outbound);
|
||||
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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;
|
||||
|
||||
/** Immutable SKU stock deltas written in the outbound pick transaction. */
|
||||
@Data
|
||||
@TableName("ll_return_inventory_outbound_movement")
|
||||
public class ReturnInventoryOutboundMovement {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long outboundPickId;
|
||||
private String customerId;
|
||||
private String overseasWarehouseId;
|
||||
private Long cartonId;
|
||||
private String cartonMark;
|
||||
private String outboundNo;
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private Long quantity;
|
||||
private Long totalBefore;
|
||||
private Long totalAfter;
|
||||
private Long availableBefore;
|
||||
private Long availableAfter;
|
||||
private Long cartonsBefore;
|
||||
private Long cartonsAfter;
|
||||
private LocalDateTime occurredAt;
|
||||
}
|
||||
+12
@@ -12,6 +12,18 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventoryBalance> {
|
||||
@Select("SELECT * FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} FOR UPDATE")
|
||||
ReturnInventoryBalance lockSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Update("UPDATE ll_return_inventory_balance SET total_quantity=total_quantity-#{quantity}, " +
|
||||
"available_quantity=available_quantity-#{quantity},carton_quantity=carton_quantity-1,updated_at=NOW(3) " +
|
||||
"WHERE id=#{id} AND customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND total_quantity>=#{quantity} AND available_quantity>=#{quantity} AND carton_quantity>=1")
|
||||
int deductOutbound(@Param("id") long id, @Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId, @Param("quantity") long quantity);
|
||||
|
||||
@Insert("INSERT INTO ll_return_inventory_balance (id,customer_id,overseas_warehouse_id,sku_code,product_name_zh,total_quantity,available_quantity,carton_quantity,created_at,updated_at) " +
|
||||
"VALUES (#{id},#{customerId},#{warehouseId},#{skuCode},#{productNameZh},#{totalQuantity},#{availableQuantity},#{cartons},NOW(3),NOW(3)) " +
|
||||
"ON DUPLICATE KEY UPDATE total_quantity=total_quantity+VALUES(total_quantity),available_quantity=available_quantity+VALUES(available_quantity),carton_quantity=carton_quantity+VALUES(carton_quantity), " +
|
||||
|
||||
+5
-3
@@ -7,15 +7,17 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Read-only return inventory movement ledger reconstructed from signed receipts and merged cartons. */
|
||||
/** Receipt/merge projections plus immutable outbound SKU movements. */
|
||||
@Mapper
|
||||
public interface ReturnInventoryMovementMapper {
|
||||
public interface ReturnInventoryMovementMapper extends com.baomidou.mybatisplus.core.mapper.BaseMapper<com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOutboundMovement> {
|
||||
String MOVEMENTS = "SELECT o.customer_id customerId,o.overseas_warehouse_id overseasWarehouseId,ri.sku_code skuCode,oi.product_name_zh productNameZh," +
|
||||
"'RECEIPT' movementType,r.receipt_no referenceNo,ri.received_quantity totalDelta,ri.received_quantity availableDelta,r.signed_at occurredAt " +
|
||||
"FROM ll_return_receipt r JOIN ll_return_order o ON o.id=r.return_order_id JOIN ll_return_receipt_item ri ON ri.receipt_id=r.id " +
|
||||
"JOIN ll_return_order_item oi ON oi.id=ri.return_order_item_id " +
|
||||
"UNION ALL SELECT c.customer_id,c.overseas_warehouse_id,i.sku_code,i.product_name_zh,'MERGE',c.carton_mark,0,-i.quantity,c.created_at " +
|
||||
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id WHERE c.source_batch_no='MERGED'";
|
||||
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id WHERE c.source_batch_no='MERGED'" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,sku_code,product_name_zh,'OUTBOUND',outbound_no," +
|
||||
"-quantity,-quantity,occurred_at FROM ll_return_inventory_outbound_movement";
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM (" + MOVEMENTS + ") m WHERE 1=1 " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND m.customerId=#{customerId} </if>" +
|
||||
|
||||
+53
@@ -60,6 +60,59 @@ public class ReturnInventoryService {
|
||||
@Resource private LastlegOrderDetailMapper lastlegOrderDetails;
|
||||
@Resource private com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway physicalPallets;
|
||||
|
||||
/** Called inside the PDA pick transaction; carton contents remain the physical shipment snapshot. */
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
public void shipCarton(String customerId, String warehouseId, String returnNo, String cartonMark,
|
||||
String outboundNo, long outboundPickId) {
|
||||
ReturnInventoryCarton carton = cartons.lockMark(required(cartonMark, "退货箱码不能为空"));
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !warehouseId.equals(carton.getOverseasWarehouseId()) || !returnNo.equals(carton.getSourceReturnNo())
|
||||
|| !AVAILABLE.equals(carton.getStatus())) throw conflict("退货箱状态或客户、仓库、来源不匹配");
|
||||
List<ReturnInventoryCartonItem> contents = cartonItems.lockCarton(carton.getId());
|
||||
long total = 0;
|
||||
var skus = new java.util.HashSet<String>();
|
||||
for (ReturnInventoryCartonItem content : contents) {
|
||||
if (!customerId.equals(content.getCustomerId()) || !carton.getId().equals(content.getCartonId())
|
||||
|| text(content.getSkuCode()) == null || !skus.add(content.getSkuCode()) || value(content.getQuantity()) <= 0) {
|
||||
throw conflict("退货箱内容缺失或归属、SKU数量不一致");
|
||||
}
|
||||
total = Math.addExact(total, content.getQuantity());
|
||||
}
|
||||
if (contents.isEmpty() || total != value(carton.getTotalQuantity())
|
||||
|| carton.getSkuTypes() == null || contents.size() != carton.getSkuTypes()) throw conflict("退货箱内容与实物数量不一致");
|
||||
contents = contents.stream().sorted(Comparator.comparing(ReturnInventoryCartonItem::getSkuCode)).toList();
|
||||
String scopedCustomer = carton.getCustomerId(), scopedWarehouse = carton.getOverseasWarehouseId();
|
||||
Map<String, com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance> lockedBalances = new LinkedHashMap<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (ReturnInventoryCartonItem content : contents) {
|
||||
var balance = balances.lockSku(scopedCustomer, scopedWarehouse, content.getSkuCode());
|
||||
long quantity = content.getQuantity();
|
||||
if (balance == null || value(balance.getTotalQuantity()) < quantity
|
||||
|| value(balance.getAvailableQuantity()) < quantity || value(balance.getCartonQuantity()) < 1) {
|
||||
throw conflict("退货库存不足,不能完成该箱出库:" + content.getSkuCode());
|
||||
}
|
||||
lockedBalances.put(content.getSkuCode(), balance);
|
||||
}
|
||||
for (ReturnInventoryCartonItem content : contents) {
|
||||
var balance = lockedBalances.get(content.getSkuCode());
|
||||
long quantity = content.getQuantity();
|
||||
var movement = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOutboundMovement();
|
||||
movement.setId(IdWorker.getId()); movement.setOutboundPickId(outboundPickId);
|
||||
movement.setCustomerId(scopedCustomer); movement.setOverseasWarehouseId(scopedWarehouse);
|
||||
movement.setCartonId(carton.getId()); movement.setCartonMark(carton.getCartonMark());
|
||||
movement.setOutboundNo(required(outboundNo, "出库单号不能为空"));
|
||||
movement.setSkuCode(content.getSkuCode()); movement.setProductNameZh(content.getProductNameZh()); movement.setQuantity(quantity);
|
||||
movement.setTotalBefore(balance.getTotalQuantity()); movement.setTotalAfter(balance.getTotalQuantity() - quantity);
|
||||
movement.setAvailableBefore(balance.getAvailableQuantity()); movement.setAvailableAfter(balance.getAvailableQuantity() - quantity);
|
||||
movement.setCartonsBefore(balance.getCartonQuantity()); movement.setCartonsAfter(balance.getCartonQuantity() - 1);
|
||||
movement.setOccurredAt(now);
|
||||
if (balances.deductOutbound(balance.getId(), scopedCustomer, scopedWarehouse, quantity) != 1) throw conflict("退货库存余额已变化,请重新核对");
|
||||
if (movements.insert(movement) != 1) throw new IllegalStateException("退货出库流水写入失败");
|
||||
}
|
||||
carton.setStatus("OUTBOUNDED"); carton.setUpdatedAt(now);
|
||||
if (cartons.updateById(carton) != 1) throw conflict("退货箱状态已变化,请重新核对");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryCarton createReceiptCarton(ReturnOrder order, ReturnReceipt receipt,
|
||||
String requestedMark, List<ReceiptLine> lines) {
|
||||
|
||||
Reference in New Issue
Block a user