feat: enforce canonical container identity across regions

This commit is contained in:
2026-08-31 12:13:20 +08:00
parent b386f35ab4
commit 942dbace19
78 changed files with 1203 additions and 730 deletions
@@ -12,9 +12,9 @@ import java.util.List;
@Mapper
public interface InboundPlanOrderMapper {
record ContainerOrderRef(String customerId, Long orderId, String currentStatus) { }
record ContainerOrderRef(String customerId, Long orderId, String currentStatus, String containerNo) { }
@Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus " +
@Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus,container_no AS containerNo " +
"FROM ll_order WHERE source_container_booking_id=#{sourceContainerBookingId} ORDER BY customer_id,id FOR UPDATE")
List<ContainerOrderRef> lockContainerOrders(@Param("sourceContainerBookingId") String sourceContainerBookingId);
@@ -19,6 +19,7 @@ import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper;
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.common.event.ContainerNumbers;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -67,10 +68,10 @@ public class InboundPlanService {
if (orders == null || orders.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound order not found");
}
String containerNo = trimToNull(orders.get(0).containerNo());
if (containerNo == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Inbound order container number is empty");
String containerNo = requiredContainerNo(orders.get(0).containerNo());
InboundPlan existing = planMapper.findBySourceOrder(customerId, selectedOrderId);
if (existing != null) {
requirePlanContainer(existing, containerNo);
return detail(customerId, existing);
}
validateOrders(orders);
@@ -79,11 +80,7 @@ public class InboundPlanService {
private InboundPlanResponse createPlan(String customerId, Long selectedOrderId,
List<SourceOrderSnapshot> orders) {
String containerNo = trimToNull(orders.get(0).containerNo());
if (containerNo == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Inbound order container number is empty");
}
String containerNo = requiredContainerNo(orders.get(0).containerNo());
LocalDateTime now = LocalDateTime.now();
InboundPlan plan = new InboundPlan();
plan.setId(IdWorker.getId());
@@ -138,6 +135,14 @@ public class InboundPlanService {
if (bookingId == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订 ID 不能为空");
List<InboundPlanOrderMapper.ContainerOrderRef> refs = orderMapper.lockContainerOrders(bookingId);
if (refs.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜没有尾程订单");
String containerNo = null;
for (InboundPlanOrderMapper.ContainerOrderRef ref : refs) {
String current = requiredContainerNo(ref.containerNo());
if (containerNo == null) containerNo = current;
if (!Objects.equals(containerNo, current)) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜内尾程订单柜号不一致");
}
}
List<InboundPlan> existingPlans = refs.stream()
.map(ref -> planMapper.findBySourceOrder(ref.customerId(), ref.orderId()))
.toList();
@@ -145,6 +150,7 @@ public class InboundPlanService {
if (existingCount == refs.size()) {
List<InboundPlanResponse> replay = new ArrayList<>(refs.size());
for (int index = 0; index < refs.size(); index++) {
requirePlanContainer(existingPlans.get(index), containerNo);
replay.add(detail(refs.get(index).customerId(), existingPlans.get(index)));
}
return List.copyOf(replay);
@@ -158,15 +164,13 @@ public class InboundPlanService {
}
List<List<SourceOrderSnapshot>> snapshots = new ArrayList<>(refs.size());
String containerNo = null;
for (InboundPlanOrderMapper.ContainerOrderRef ref : refs) {
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(ref.customerId(), ref.orderId());
if (orders == null || orders.isEmpty()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜订单在锁定后不存在");
}
validateOrders(orders);
String currentContainerNo = trimToNull(orders.get(0).containerNo());
if (containerNo == null) containerNo = currentContainerNo;
String currentContainerNo = requiredContainerNo(orders.get(0).containerNo());
if (!Objects.equals(containerNo, currentContainerNo)) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜内尾程订单柜号不一致");
}
@@ -181,6 +185,22 @@ public class InboundPlanService {
return List.copyOf(created);
}
private String requiredContainerNo(String value) {
try {
return ContainerNumbers.requireCanonical(value);
} catch (IllegalArgumentException exception) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"来源柜真实集装箱号缺失或格式无效", exception);
}
}
private void requirePlanContainer(InboundPlan plan, String containerNo) {
String frozen = requiredContainerNo(plan.getContainerNo());
if (!Objects.equals(containerNo, frozen)) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "入库计划柜号快照与来源柜不一致");
}
}
@Transactional(readOnly = true)
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {