This commit is contained in:
2026-08-27 16:34:30 +08:00
parent 1db3e48506
commit ecf48886d5
10 changed files with 423 additions and 5 deletions
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
@@ -52,6 +53,18 @@ public class InboundPlanController {
return ApiResponse.success(service.convert(convertScope(user, request.customerId()), request.orderId()));
}
@PostMapping("/convert-container")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_CREATE + "')")
@Operation(summary = "按整柜批量生成入库计划", description = "平台按 sourceContainerBookingId 锁定整柜订单;每个入仓码独立生成计划,整批原子且可幂等重放。Required authority: " + OverseasPermissions.INBOUND_ORDER_CREATE)
public ApiResponse<java.util.List<InboundPlanResponse>> convertContainer(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ConvertContainerInboundPlanRequest request) {
if (user == null || user.tenantId() == null || user.tenantId() != 0L || !"PLATFORM".equals(user.appCode())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "仅平台账号可按整柜生成入库计划");
}
return ApiResponse.success(service.convertContainer(request.sourceContainerBookingId()));
}
@GetMapping("/{planNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
/** 查询入仓计划及其明细、托盘和分配信息。 */
@@ -0,0 +1,10 @@
package com.baiyue.lastleg.inboundplan.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
/** 平台按来源头程柜预订身份批量生成入库计划。 */
public record ConvertContainerInboundPlanRequest(
@NotBlank @Size(max = 128) String sourceContainerBookingId
) {
}
@@ -12,6 +12,12 @@ import java.util.List;
@Mapper
public interface InboundPlanOrderMapper {
record ContainerOrderRef(String customerId, Long orderId, String currentStatus) { }
@Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus " +
"FROM ll_order WHERE source_container_booking_id=#{sourceContainerBookingId} ORDER BY customer_id,id FOR UPDATE")
List<ContainerOrderRef> lockContainerOrders(@Param("sourceContainerBookingId") String sourceContainerBookingId);
@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);
@@ -121,6 +121,30 @@ public class InboundPlanService {
return detail(customerId, plan);
}
/** 同一来源柜的每个尾程订单独立建计划;整个柜级批次共用一个事务。 */
@Transactional
public List<InboundPlanResponse> convertContainer(String sourceContainerBookingId) {
String bookingId = trimToNull(sourceContainerBookingId);
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, "来源柜没有尾程订单");
boolean hasEligible = refs.stream().anyMatch(ref -> "PENDING_ASSIGNMENT".equals(ref.currentStatus())
|| "DESTINATION_ASSIGNED".equals(ref.currentStatus()));
if (!hasEligible) {
List<InboundPlanResponse> replay = refs.stream().map(ref -> {
InboundPlan existing = planMapper.findBySourceOrder(ref.customerId(), ref.orderId());
if (existing == null) throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜转换状态不一致");
return detail(ref.customerId(), existing);
}).toList();
return replay;
}
if (refs.stream().anyMatch(ref -> !("PENDING_ASSIGNMENT".equals(ref.currentStatus())
|| "DESTINATION_ASSIGNED".equals(ref.currentStatus())))) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜存在不能转换的尾程订单");
}
return refs.stream().map(ref -> convert(ref.customerId(), ref.orderId())).toList();
}
@Transactional(readOnly = true)
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
@@ -172,6 +172,21 @@ paths:
customerId: { type: string, pattern: '^[1-9][0-9]*$' }
orderId: { type: string, pattern: '^[1-9][0-9]*$' }
responses: { '200': { description: 统一 ApiResponse 包装的入库计划详情 } }
/api/platform/v1/last-leg/inbound-plans/convert-container:
post:
summary: 按整柜批量生成入库计划
description: 权限 platform:overseas-warehouse:inbound-order:create;使用稳定 sourceContainerBookingId 锁定整柜订单。每个尾程订单/入仓码独立生成一个入库计划,整批原子且幂等。
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [sourceContainerBookingId]
properties:
sourceContainerBookingId: { type: string, minLength: 1, maxLength: 128 }
responses: { '200': { description: 统一 ApiResponse 包装的入库计划数组 } }
/api/platform/v1/last-leg/dispatches:
get:
summary: 分页查询排车单