提交
This commit is contained in:
+22
@@ -5,11 +5,22 @@ import org.apache.ibatis.annotations.Select;
|
||||
@Mapper public interface InboundSnapshotMapper {
|
||||
record SourceCarton(String cartonMark, String skuCode, String productNameZh, Long quantity,
|
||||
String destinationWarehouseCode, String destinationWarehouseName, String skuItemsJson) {}
|
||||
/** 查询来源单当前仍可转运的箱级明细,排除已经存在有效换标记录的原箱。 */
|
||||
@Select("""
|
||||
SELECT c.carton_mark cartonMark,c.sku_code skuCode,NULL productNameZh,NULL quantity,
|
||||
c.destination_warehouse_code destinationWarehouseCode,c.destination_warehouse_name destinationWarehouseName,NULL skuItemsJson
|
||||
FROM ll_inbound_plan_carton c JOIN ll_order o ON o.id=c.order_id AND o.customer_id=c.customer_id
|
||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND o.overseas_warehouse_id=#{warehouseId}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ll_relabel_item ri
|
||||
JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id
|
||||
WHERE ri.customer_id=c.customer_id AND ri.source_inbound_code=o.inbound_code
|
||||
AND ra.overseas_warehouse_id=#{warehouseId}
|
||||
AND UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(c.carton_mark))
|
||||
AND ra.status NOT IN ('CANCELLED','REJECTED')
|
||||
)
|
||||
|
||||
|
||||
UNION ALL
|
||||
SELECT c.carton_mark,i.sku_code,i.product_name_zh,c.counted_quantity,
|
||||
t.destination_warehouse_code,t.destination_warehouse_name,c.sku_items_json
|
||||
@@ -18,6 +29,17 @@ import org.apache.ibatis.annotations.Select;
|
||||
JOIN ll_order o ON o.id=t.lastleg_order_id AND o.customer_id=t.customer_id
|
||||
JOIN ll_warehouse_transfer_item i ON i.id=c.transfer_item_id AND i.customer_id=c.customer_id AND i.transfer_order_id=t.id
|
||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND t.operating_warehouse_id=#{warehouseId} AND c.status='COUNTED'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ll_relabel_item ri
|
||||
JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id
|
||||
WHERE ri.customer_id=c.customer_id AND ri.source_inbound_code=o.inbound_code
|
||||
AND ra.overseas_warehouse_id=#{warehouseId}
|
||||
AND (UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(c.carton_mark))
|
||||
OR UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(NULLIF(c.customer_carton_mark,''))))
|
||||
AND ra.status NOT IN ('CANCELLED','REJECTED')
|
||||
)
|
||||
|
||||
|
||||
ORDER BY cartonMark
|
||||
""")
|
||||
java.util.List<SourceCarton> sourceCartons(@Param("customerId") String customerId,
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.transfer.mapper;
|
||||
|
||||
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 TransferSourceReservationMapper {
|
||||
/** 按同一入仓快照串行提交,防止不同草稿同时抢占原箱。 */
|
||||
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND id=#{inboundOrderId} FOR UPDATE")
|
||||
Long lockInbound(@Param("customerId") String customerId, @Param("inboundOrderId") Long inboundOrderId);
|
||||
|
||||
/** 读取已提交转运的原箱;驳回、取消和草稿不占用,可重新选择。 */
|
||||
@Select("SELECT COALESCE(NULLIF(c.customer_carton_mark,''), c.carton_mark) FROM ll_warehouse_transfer_carton c "
|
||||
+ "JOIN ll_warehouse_transfer_order t ON t.id=c.transfer_order_id AND t.customer_id=c.customer_id "
|
||||
+ "WHERE t.customer_id=#{customerId} AND t.operating_warehouse_id=#{warehouseId} "
|
||||
+ "AND t.inbound_code=#{sourceCode} AND t.transfer_no <> t.inbound_code "
|
||||
+ "AND t.status NOT IN ('DRAFT','REJECTED','CANCELLED') "
|
||||
+ "AND COALESCE(NULLIF(c.customer_carton_mark,''), c.carton_mark) IS NOT NULL")
|
||||
List<String> occupiedMarks(@Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId, @Param("sourceCode") String sourceCode);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.transfer.repository;
|
||||
|
||||
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
/** 封装库存转运来源箱查询,SQL和客户隔离由Mapper统一执行。 */
|
||||
@Repository
|
||||
public class TransferInventorySourceRepository {
|
||||
@Resource private InboundSnapshotMapper snapshots;
|
||||
@Resource private com.baiyue.lastleg.transfer.mapper.TransferSourceReservationMapper reservations;
|
||||
|
||||
/** 同一来源入仓单的所有库存转运先取相同的行锁。 */
|
||||
public Long lockSource(String customerId, Long inboundOrderId) {
|
||||
return reservations.lockInbound(customerId, inboundOrderId);
|
||||
}
|
||||
|
||||
/** 读取当前客户及操作仓中已提交转运的原箱快照。 */
|
||||
public List<String> occupiedMarks(String customerId, String warehouseId, String sourceCode) {
|
||||
return reservations.occupiedMarks(customerId, warehouseId, sourceCode);
|
||||
}
|
||||
|
||||
/** 读取当前来源单未被有效换标申请使用的箱级快照。 */
|
||||
public List<InboundSnapshotMapper.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||
return snapshots.sourceCartons(customerId, warehouseId, inboundCode);
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -6,6 +6,7 @@ import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@@ -17,6 +18,7 @@ public class TransferReturnSourceRepository {
|
||||
@Resource private ReturnOrderMapper orders;
|
||||
@Resource private ReturnInventoryCartonMapper cartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper items;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
|
||||
/** 按当前客户和退货单号查询来源单据。 */
|
||||
public ReturnOrder findOrder(String customerId, String returnNo) {
|
||||
@@ -25,10 +27,15 @@ public class TransferReturnSourceRepository {
|
||||
|
||||
/** 查询当前仓内已完成清点且可用的退货箱,不影响其他页面的历史库存查询。 */
|
||||
public List<ReturnInventoryCarton> availableCartons(String customerId, String warehouseId, String returnNo) {
|
||||
return cartons.currentReturnCartons(customerId, warehouseId, returnNo).stream()
|
||||
var available = cartons.currentReturnCartons(customerId, warehouseId, returnNo).stream()
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && "CONFIRMED".equals(carton.getCountStatus())
|
||||
&& carton.getRelabelApplicationId() == null)
|
||||
.toList();
|
||||
if (available.isEmpty()) return available;
|
||||
// 已完成换标的箱虽然释放了当前占用字段,但不能再次作为原箱来源转运。
|
||||
var completed = new java.util.HashSet<>(relabelItems.completedReturnCartonIds(customerId,
|
||||
available.stream().map(ReturnInventoryCarton::getId).toList()));
|
||||
return available.stream().filter(carton -> !completed.contains(carton.getId())).toList();
|
||||
}
|
||||
|
||||
/** 批量读取已通过客户及仓库隔离校验的来源箱明细。 */
|
||||
|
||||
+51
-6
@@ -149,6 +149,7 @@ public class WarehouseTransferService {
|
||||
@Resource private InventoryCommandMapper inventoryCommands;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private TransferReturnSourceRepository returnSources;
|
||||
@Resource private com.baiyue.lastleg.transfer.repository.TransferInventorySourceRepository inventorySources;
|
||||
|
||||
/** 查询来源单据当前可用的箱唛及箱内 SKU,兼容普通入仓与退货库存。 */
|
||||
public List<WarehouseTransferResponses.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||
@@ -164,22 +165,36 @@ public class WarehouseTransferService {
|
||||
List<Long> cartonIds = cartons.stream().map(ReturnInventoryCarton::getId).filter(Objects::nonNull).toList();
|
||||
Map<Long, List<ReturnInventoryCartonItem>> items = cartonIds.isEmpty() ? Map.of()
|
||||
: returnSources.cartonItems(cartonIds).stream().collect(Collectors.groupingBy(ReturnInventoryCartonItem::getCartonId));
|
||||
return cartons.stream().map(carton -> {
|
||||
return unreservedSourceCartons(customerId, warehouseId.trim(), source, cartons.stream().map(carton -> {
|
||||
List<ReturnInventoryCartonItem> rows = items.getOrDefault(carton.getId(), List.of());
|
||||
List<WarehouseTransferResponses.SourceCartonSku> contents = rows.stream()
|
||||
.filter(item -> item.getQuantity() != null && item.getQuantity() > 0)
|
||||
.map(item -> new WarehouseTransferResponses.SourceCartonSku(item.getSkuCode(), item.getQuantity())).toList();
|
||||
return new WarehouseTransferResponses.SourceCarton(carton.getCartonMark(), null, carton.getTotalQuantity(),
|
||||
carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), contents);
|
||||
}).toList();
|
||||
}).toList());
|
||||
}
|
||||
return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), source).stream().map(row -> {
|
||||
var sourceRows = inventorySources != null
|
||||
? inventorySources.sourceCartons(customerId, warehouseId.trim(), source)
|
||||
: inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), source);
|
||||
return unreservedSourceCartons(customerId, warehouseId.trim(), source, sourceRows.stream().map(row -> {
|
||||
var carton = new WarehouseTransferCarton(); carton.setSkuItemsJson(row.skuItemsJson());
|
||||
var contents = cartonContents(carton).stream().map(item -> new WarehouseTransferResponses.SourceCartonSku(item.skuCode(),item.quantity())).toList();
|
||||
if (contents.isEmpty()) contents = List.of(new WarehouseTransferResponses.SourceCartonSku(row.skuCode(),row.quantity()));
|
||||
return new WarehouseTransferResponses.SourceCarton(row.cartonMark(),row.productNameZh(),row.quantity(),
|
||||
row.destinationWarehouseCode(),row.destinationWarehouseName(),contents);
|
||||
}).toList();
|
||||
}).toList());
|
||||
}
|
||||
|
||||
/** 待审核即按原箱快照占用;所有来源查询复用此过滤,避免重复提交同一实物箱。 */
|
||||
private List<WarehouseTransferResponses.SourceCarton> unreservedSourceCartons(String customerId,
|
||||
String warehouseId, String source, List<WarehouseTransferResponses.SourceCarton> cartons) {
|
||||
if (inventorySources == null) return cartons;
|
||||
Set<String> occupied = inventorySources.occupiedMarks(customerId, warehouseId, source).stream()
|
||||
.filter(StringUtils::hasText).map(mark -> mark.trim().toUpperCase(java.util.Locale.ROOT))
|
||||
.collect(Collectors.toSet());
|
||||
return cartons.stream().filter(box -> !occupied.contains(box.cartonMark().trim()
|
||||
.toUpperCase(java.util.Locale.ROOT))).toList();
|
||||
}
|
||||
private static LocalDateTime arrivalTime(WarehouseTransferRequests.Create request) {
|
||||
return StringUtils.hasText(request.inboundCode()) ? null : request.estimatedArrivalAt();
|
||||
@@ -334,7 +349,7 @@ public class WarehouseTransferService {
|
||||
}
|
||||
|
||||
/** Platform-created transfers keep the existing create-and-submit behavior. */
|
||||
@Transactional
|
||||
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public WarehouseTransferResponses.Detail createAndSubmit(String customerId, String operatorId,
|
||||
WarehouseTransferRequests.Create request) {
|
||||
WarehouseTransferResponses.Detail draft = create(customerId, operatorId, request);
|
||||
@@ -456,7 +471,7 @@ public class WarehouseTransferService {
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
/** 提交草稿前重新校验来源箱归属,防止草稿保存后库存已发生变化。 */
|
||||
public WarehouseTransferResponses.Detail submit(String customerId, String transferNo, String operatorId) {
|
||||
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
||||
@@ -464,6 +479,13 @@ public class WarehouseTransferService {
|
||||
if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo);
|
||||
if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted");
|
||||
|
||||
if (!transfer.getTransferNo().equals(transfer.getInboundCode())) {
|
||||
// 所有同源提交锁定同一入仓记录;取锁后按最新可用箱校验并保存逐箱占用快照。
|
||||
if (inventorySources != null && inventorySources.lockSource(customerId, transfer.getInboundOrderId()) == null)
|
||||
throw conflict("库存来源已失效,请重新选择来源单");
|
||||
if (inventorySources != null) reserveSourceCartons(customerId, transfer);
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
Set<String> allowedMarks = !transfer.getTransferNo().equals(transfer.getInboundCode())
|
||||
? sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())
|
||||
@@ -500,6 +522,29 @@ public class WarehouseTransferService {
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
/** 提交时逐箱核对来源快照并记录占用,禁止并发提交时静默替换用户选择。 */
|
||||
private void reserveSourceCartons(String customerId, WarehouseTransferOrder transfer) {
|
||||
var available = new java.util.LinkedHashMap<String, String>();
|
||||
for (var box : sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())) {
|
||||
available.put(box.cartonMark().trim().toUpperCase(java.util.Locale.ROOT), box.cartonMark());
|
||||
}
|
||||
var selected = transferCartons.listOrder(customerId, transfer.getId());
|
||||
if (selected.size() != transfer.getPackageCount() || selected.isEmpty())
|
||||
throw conflict("转运箱数与来源箱明细不一致,请刷新后重新提交");
|
||||
var reserved = new java.util.LinkedHashMap<Long, String>();
|
||||
for (var carton : selected) {
|
||||
String mark = StringUtils.hasText(carton.getCustomerCartonMark())
|
||||
? carton.getCustomerCartonMark() : carton.getCartonMark();
|
||||
if (!StringUtils.hasText(mark)) throw conflict("转运箱缺少来源箱号,请刷新来源单");
|
||||
String canonical = available.remove(mark.trim().toUpperCase(java.util.Locale.ROOT));
|
||||
if (canonical == null) throw conflict("来源箱已被提交或不可用,请刷新来源单:" + mark);
|
||||
reserved.put(carton.getId(), canonical);
|
||||
}
|
||||
for (var carton : selected) {
|
||||
carton.setCustomerCartonMark(reserved.get(carton.getId()));
|
||||
transferCartons.updateById(carton);
|
||||
}
|
||||
}
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<WarehouseTransferResponses.Summary> page(String customerId, WarehouseTransferRequests.Query query) {
|
||||
return page(customerId, query, true);
|
||||
|
||||
Reference in New Issue
Block a user