This commit is contained in:
2026-10-19 17:59:34 +08:00
parent dcfba1053c
commit ce189f062f
14 changed files with 305 additions and 6 deletions
@@ -0,0 +1,66 @@
package com.baiyue.lastleg.discard.repository;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
import com.baiyue.lastleg.inventory.entity.InventoryLedger;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryLedgerMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryBalanceMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Repository;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
/** 废弃实物扣减对应的退货余额和普通库存总账。 */
@Repository
public class CargoDiscardStockLedgerRepository {
@Resource private ReturnInventoryBalanceMapper returnBalances;
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private InventoryBalanceMapper balances;
@Resource private InventoryLedgerMapper ledgers;
/** 箱内 SKU 扣减时同步扣汇总,只有该 SKU 在箱内耗尽才减一箱。 */
public void deductReturn(String customerId, CargoDiscardApplication application, CargoDiscardItem item,
long before, long quantity) {
var carton = returnCartons.lockId(item.getReturnCartonId());
if (carton == null || !customerId.equals(carton.getCustomerId())
|| !application.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())
|| !"DISCARD_PENDING".equals(carton.getStatus())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "退货废弃箱归属或状态已变化");
}
long boxes = before == quantity && !"PARCEL".equals(carton.getSourceUnitType()) ? 1 : 0;
var balance = returnBalances.lockSku(customerId, carton.getOverseasWarehouseId(), item.getSkuCode());
if (balance == null || returnBalances.deductConversion(customerId, carton.getOverseasWarehouseId(),
item.getSkuCode(), quantity, boxes) != 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "退货库存汇总数量不足,不能完成废弃");
}
}
/** 一件代发已扣实物仓位后,按同事务余额快照追加库存总账。 */
public void recordDropship(String customerId, CargoDiscardApplication application, CargoDiscardItem item,
InventoryPosition position, long quantity, String operator, LocalDateTime at) {
var balance = balances.selectById(position.getBalanceId());
if (balance == null || !customerId.equals(balance.getCustomerId())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "废弃库存余额不存在");
}
var fact = new InventoryLedger();
fact.setId(IdWorker.getId()); fact.setCustomerId(customerId); fact.setCustomerName(balance.getCustomerName());
fact.setBalanceId(balance.getId()); fact.setPositionId(position.getId());
fact.setOverseasWarehouseId(balance.getOverseasWarehouseId()); fact.setDestinationWarehouseCode(balance.getDestinationWarehouseCode());
fact.setInboundOrderId(balance.getInboundOrderId()); fact.setInboundCode(balance.getInboundCode());
fact.setInboundOrderNo(balance.getInboundOrderNo()); fact.setPalletId(position.getPalletId());
fact.setPalletNo(position.getPalletNo()); fact.setBatchNo(position.getBatchNo());
fact.setWarehouseLocationCode(position.getWarehouseLocationCode()); fact.setSkuCode(item.getSkuCode());
fact.setBusinessType("DISCARD"); fact.setBusinessOrderId(application.getId()); fact.setBusinessOrderNo(application.getDiscardNo());
fact.setIdempotencyKey("CARGO_DISCARD:" + application.getId() + ":" + item.getId() + ":STOCK");
fact.setStatusFrom("AVAILABLE"); fact.setStatusTo("DISCARDED");
fact.setBeforePackages(balance.getAvailablePackages() + quantity); fact.setDeltaPackages(-quantity);
fact.setAfterPackages(balance.getAvailablePackages()); fact.setOperatorId(operator);
fact.setRemark("货物废弃"); fact.setOccurredAt(at); fact.setCreatedAt(at);
if (ledgers.insert(fact) != 1) throw new IllegalStateException("废弃库存流水写入失败");
}
}
@@ -33,6 +33,7 @@ import java.util.Map;
/** Final inventory mutation for a warehouse-approved cargo discard. */
@Service
public class CargoDiscardInventoryAdjustmentService {
@Resource private com.baiyue.lastleg.discard.repository.CargoDiscardStockLedgerRepository stockLedger;
@Resource private CargoDiscardInventoryMapper inventory;
@Resource private CargoDiscardStockService stockService;
@Resource private DropshipCartonLedgerMapper cartonLedgers;
@@ -44,6 +45,7 @@ public class CargoDiscardInventoryAdjustmentService {
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
/** 实物废弃、来源库存扣减与流水在同一事务内完成。 */
@Transactional(propagation = Propagation.MANDATORY)
public AdjustmentResult discardPalletItem(String customerId, CargoDiscardApplication application,
CargoDiscardItem item, List<CargoDiscardCarton> links,
@@ -53,7 +55,7 @@ public class CargoDiscardInventoryAdjustmentService {
stockService.discard(customerId, operatorId, application, item, actualPackages);
return new AdjustmentResult(item.getDiscardPackages(), actualPackages, 0);
}
if (isReturnItem(item)) return discardReturnItem(customerId, item, actualPackages);
if (isReturnItem(item)) return discardReturnItem(customerId, application, item, actualPackages);
if (actualPackages < 0 || actualPackages > links.size()) throw conflict("实际废弃数量超出该托盘申请数量");
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode());
@@ -97,10 +99,12 @@ public class CargoDiscardInventoryAdjustmentService {
}
long after = value(inventory.skuPackages(customerId, sku.getId()));
if (after != before - actualPackages) throw conflict("托盘 SKU 库存扣减不守恒");
stockLedger.recordDropship(customerId, application, item, position, actualPackages, operatorId, occurredAt);
return new AdjustmentResult(before, actualPackages, after);
}
private AdjustmentResult discardReturnItem(String customerId, CargoDiscardItem item, int actualPackages) {
/** 退货箱按件扣减,同时同步库存余额;完成事实由明细的 PDA 完成快照提供。 */
private AdjustmentResult discardReturnItem(String customerId, CargoDiscardApplication application, CargoDiscardItem item, int actualPackages) {
if (actualPackages < 0 || actualPackages > item.getDiscardPackages()) {
throw conflict("实际废弃数量超出退货库存申请数量");
}
@@ -110,6 +114,7 @@ public class CargoDiscardInventoryAdjustmentService {
if (before < actualPackages || (actualPackages > 0 && returnCartonItems.deduct(sku.getId(), actualPackages) != 1)) {
throw conflict("退货库存数量不足");
}
if (actualPackages > 0) stockLedger.deductReturn(customerId, application, item, before, actualPackages);
return new AdjustmentResult(before, actualPackages, before - actualPackages);
}
@@ -26,7 +26,10 @@ public final class ReturnInventoryResponses {
int skuTypes, long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height,
BigDecimal weight, LocalDateTime receivedAt, List<CartonItem> items,
String locationCode, String palletCode, LocalDateTime putawayAt, List<MergeHistory> mergeHistory,
long relabelledQuantity, long relabelReservedQuantity, List<SkuConversion> skuConversions) { }
long relabelledQuantity, long relabelReservedQuantity, List<SkuConversion> skuConversions,
List<DiscardHistory> discardHistory) { }
public record DiscardHistory(String discardNo, String skuCode, String productNameZh, long beforeQuantity,
long quantity, long afterQuantity, String operatorId, LocalDateTime occurredAt) { }
public record SkuConversion(String relabelNo, String cartonMark, String oldSkuCode, String newSkuCode,
long quantity, String status, LocalDateTime completedAt) {
public static SkuConversion from(com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion row) {
@@ -0,0 +1,27 @@
package com.baiyue.lastleg.returnreceipt.mapper;
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/** 从不可重复执行的 PDA 完成快照读取退货废弃事实,兼容已完成的历史单据。 */
@Mapper
public interface ReturnDiscardMovementMapper {
String FACTS = " FROM ll_cargo_discard_item i JOIN ll_cargo_discard_application a " +
"ON a.id=i.application_id AND a.customer_id=i.customer_id " +
"JOIN ll_return_inventory_carton c ON c.id=i.return_carton_id AND c.customer_id=i.customer_id " +
"AND c.overseas_warehouse_id=a.overseas_warehouse_id " +
"WHERE i.pda_status='COMPLETED' AND i.actual_discard_packages>0 ";
String MOVEMENTS = "SELECT i.customer_id customerId,a.overseas_warehouse_id overseasWarehouseId,i.sku_code skuCode,i.product_name productNameZh," +
"'DISCARD' movementType,a.discard_no referenceNo,-i.actual_discard_packages totalDelta,-i.actual_discard_packages availableDelta,i.pda_completed_at occurredAt" + FACTS;
/** 按客户与实物箱读取每次废弃的件数、操作者和操作时间。 */
@Select("SELECT a.discard_no discardNo,i.sku_code skuCode,i.product_name productNameZh," +
"i.available_packages_snapshot beforeQuantity,i.actual_discard_packages quantity," +
"i.available_packages_snapshot-i.actual_discard_packages afterQuantity," +
"i.pda_operator_id operatorId,i.pda_completed_at occurredAt" + FACTS +
"AND i.customer_id=#{customerId} AND i.return_carton_id=#{cartonId} ORDER BY i.pda_completed_at DESC,i.id DESC")
List<ReturnInventoryResponses.DiscardHistory> cartonHistory(@Param("customerId") String customerId, @Param("cartonId") long cartonId);
}
@@ -22,7 +22,8 @@ public interface ReturnInventoryMovementMapper extends com.baomidou.mybatisplus.
" UNION ALL SELECT customer_id,overseas_warehouse_id,old_sku_code,product_name_zh,'SKU_CONVERSION_OUT',relabel_no,-quantity,-quantity,completed_at FROM ll_return_sku_conversion" +
" UNION ALL SELECT customer_id,overseas_warehouse_id,new_sku_code,product_name_zh,'SKU_CONVERSION_IN',relabel_no,quantity,quantity,completed_at FROM ll_return_sku_conversion" +
" UNION ALL SELECT customer_id,overseas_warehouse_id,old_sku_code,product_name_zh,'SKU_CONVERSION_REVERSE_IN',relabel_no,quantity,quantity,reversed_at FROM ll_return_sku_conversion WHERE status='REVERSED'" +
" UNION ALL SELECT customer_id,overseas_warehouse_id,new_sku_code,product_name_zh,'SKU_CONVERSION_REVERSE_OUT',relabel_no,-quantity,-quantity,reversed_at FROM ll_return_sku_conversion WHERE status='REVERSED'";
" UNION ALL SELECT customer_id,overseas_warehouse_id,new_sku_code,product_name_zh,'SKU_CONVERSION_REVERSE_OUT',relabel_no,-quantity,-quantity,reversed_at FROM ll_return_sku_conversion WHERE status='REVERSED'" +
" UNION ALL " + ReturnDiscardMovementMapper.MOVEMENTS;
@Select("<script>SELECT COUNT(*) FROM (" + MOVEMENTS + ") m WHERE 1=1 " +
"<if test='customerId != null and customerId != \"\"'>AND m.customerId=#{customerId} </if>" +
@@ -0,0 +1,18 @@
package com.baiyue.lastleg.returnreceipt.repository;
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnDiscardMovementMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
import java.util.List;
/** 退货箱废弃历史的持久化查询入口。 */
@Repository
public class ReturnDiscardMovementRepository {
@Resource private ReturnDiscardMovementMapper movements;
/** 仅返回指定客户实物箱已经完成的废弃事实。 */
public List<ReturnInventoryResponses.DiscardHistory> cartonHistory(String customerId, long cartonId) {
return movements.cartonHistory(customerId, cartonId);
}
}
@@ -63,6 +63,7 @@ public class ReturnInventoryService {
@Resource private ReturnInventoryOperationMapper operations;
@Resource private ReturnInventoryQueryMapper queries;
@Resource private ReturnInventoryMovementMapper movements;
@Resource private com.baiyue.lastleg.returnreceipt.repository.ReturnDiscardMovementRepository discardMovements;
@Resource private LastlegOrderNoGenerator numbers;
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper orders;
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderItemMapper orderItems;
@@ -1242,6 +1243,7 @@ public class ReturnInventoryService {
return "PENDING".equals(carton.getCountStatus()) && AVAILABLE.equals(carton.getStatus()) ? "PENDING_COUNT" : carton.getStatus();
}
/** 组装箱内余量及合箱、换标、废弃操作历史。 */
private ReturnInventoryResponses.CartonDetail cartonDetail(ReturnInventoryCarton carton) {
List<ReturnInventoryResponses.CartonItem> items = cartonItems.listCarton(carton.getId()).stream()
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity(),
@@ -1253,7 +1255,8 @@ public class ReturnInventoryService {
return new ReturnInventoryResponses.CartonDetail(carton.getCartonMark(), carton.getCustomerId(), carton.getOverseasWarehouseId(), carton.getSourceBatchNo(), carton.getSourceReturnNo(),
displayStatus(carton), carton.getFbaCartonMark(), carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), carton.getRelabelNo(), relabelProgressStatus(carton), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(), items, carton.getLocationCode(), carton.getPalletCode(), carton.getPutawayAt(), packingTrace.listCartonHistory(carton.getCustomerId(), carton.getCartonMark()),
completedRelabelQuantity(carton), cartons.relabelReservedQuantity(carton.getId()),
skuConversions.listCarton(carton.getCustomerId(), carton.getId()).stream().map(ReturnInventoryResponses.SkuConversion::from).toList());
skuConversions.listCarton(carton.getCustomerId(), carton.getId()).stream().map(ReturnInventoryResponses.SkuConversion::from).toList(),
discardMovements.cartonHistory(carton.getCustomerId(), carton.getId()));
}
private String relabelProgressStatus(ReturnInventoryCarton carton) {