fix: auto materialize completed container plans

This commit is contained in:
2026-09-10 17:05:40 +08:00
parent 343c6e278a
commit d37e6cf147
7 changed files with 121 additions and 5 deletions
@@ -49,7 +49,7 @@ public class InboundPlanController {
@PostMapping("/manual")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_CREATE + "')")
@Operation(summary = "手工填写并创建单票入仓计划", description = "要求订单版本和 Idempotency-Key;仓库主档、入仓码及客户归属均由服务端校验。Required authority: "
@Operation(summary = "手工填写并创建单票入仓计划", description = "要求订单版本和 Idempotency-Key;仓库主档、入仓码及客户归属均由服务端校验。整柜最后一票计划保存后,服务端自动生成并绑定全柜托盘码。Required authority: "
+ OverseasPermissions.INBOUND_ORDER_CREATE)
public ApiResponse<InboundPlanResponse> manual(@AuthenticationPrincipal AuthenticatedUser user,
@RequestHeader("Idempotency-Key") String idempotencyKey,
@@ -59,7 +59,8 @@ public class InboundPlanController {
}
convertScope(user, request.customerId());
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
return ApiResponse.success(service.convertManual(request, idempotencyKey, operatorId));
return ApiResponse.success(containerConversion.convertManualAndAutoMaterialize(
request, idempotencyKey, operatorId));
}
@PostMapping("/convert-container")
@@ -67,6 +67,11 @@ public interface InboundPlanOrderMapper {
@Select("SELECT container_no FROM ll_order WHERE customer_id=#{customerId} AND id=#{orderId} LIMIT 1")
String findContainerNo(@Param("customerId") String customerId, @Param("orderId") Long orderId);
@Select("SELECT source_container_booking_id FROM ll_order " +
"WHERE customer_id=#{customerId} AND id=#{orderId} LIMIT 1")
String findSourceContainerBookingId(@Param("customerId") String customerId,
@Param("orderId") Long orderId);
@Select("SELECT o.id AS orderId,o.customer_id AS customerId,o.container_no AS containerNo," +
"o.inbound_code AS inboundCode,o.current_status AS currentStatus,o.inbound_order_no AS inboundOrderNo," +
"COALESCE(o.volume_cbm,0) AS volumeCbm,COALESCE(o.package_count,0) AS packageCount," +
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.inboundplan.dto.ContainerInboundPlanPreviewResponse;
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.entity.ContainerPlanConversionCommand;
import com.baiyue.lastleg.inboundplan.mapper.ContainerPlanConversionCommandMapper;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
@@ -46,6 +47,35 @@ public class ContainerInboundPlanConversionCoordinator {
@Resource private ObjectMapper objectMapper;
@Resource private PlatformTransactionManager transactionManager;
/**
* Saves one manually adjusted plan, then materializes every plan in the container as soon as
* the last logical plan is complete. The manual endpoint is therefore never a separate pallet
* creation workflow: for a one-order container this happens immediately; for a multi-order
* container the final manual save closes the whole container.
*/
public InboundPlanResponse convertManualAndAutoMaterialize(ManualInboundPlanRequest request,
String idempotencyKey,
String operatorId) {
InboundPlanResponse manual = plans.convertManual(request, idempotencyKey, operatorId);
String bookingId = plans.sourceContainerBookingId(manual.customerId(), manual.sourceOrderId());
ContainerInboundPlanPreviewResponse preview = plans.previewContainer(bookingId);
List<ContainerInboundPlanPreviewResponse.Order> orders = preview.groups().stream()
.flatMap(group -> group.orders().stream()).toList();
boolean allLogicalPlansComplete = !orders.isEmpty() && orders.stream().allMatch(order ->
"PLANNED".equals(order.readiness()) || "RESOURCE_PENDING".equals(order.readiness()));
boolean needsMaterialization = orders.stream()
.anyMatch(order -> "RESOURCE_PENDING".equals(order.readiness()));
if (!preview.ready() || !allLogicalPlansComplete || !needsMaterialization) return manual;
List<InboundPlanResponse> materialized = confirm(new ConvertContainerInboundPlanRequest(
bookingId, preview.snapshotVersion(), preview.snapshotHash()),
"AUTO-MANUAL:" + hash(List.of(bookingId, idempotencyKey)));
return materialized.stream()
.filter(plan -> Objects.equals(plan.customerId(), manual.customerId())
&& Objects.equals(plan.sourceOrderId(), manual.sourceOrderId()))
.findFirst().orElse(manual);
}
public List<InboundPlanResponse> confirm(ConvertContainerInboundPlanRequest request, String idempotencyKey) {
String bookingId = required(request.sourceContainerBookingId(), 128, "来源柜预订 ID 不能为空");
String key = required(idempotencyKey, 128, "Idempotency-Key 必须为1至128个字符");
@@ -76,6 +76,15 @@ public class InboundPlanService {
: sourcePalletMapper.palletNos(customerId, orderId);
}
@Transactional(readOnly = true)
public String sourceContainerBookingId(String customerId, long orderId) {
String bookingId = trimToNull(orderMapper.findSourceContainerBookingId(customerId, orderId));
if (bookingId == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "尾程订单不存在或未绑定来源柜");
}
return bookingId;
}
@Transactional
public InboundPlanResponse convertManual(ManualInboundPlanRequest request, String idempotencyKey,
String operatorId) {