fix(lastleg): unblock inbound plan handoff

This commit is contained in:
2026-08-27 15:56:14 +08:00
parent adabec13d3
commit 0f2e201c38
28 changed files with 378 additions and 103 deletions
@@ -49,7 +49,7 @@ public class InboundPlanController {
+ OverseasPermissions.INBOUND_ORDER_CREATE)
public ApiResponse<InboundPlanResponse> convert(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ConvertInboundPlanRequest request) {
return ApiResponse.success(service.convert(scope(user, request.customerId()), request.orderId()));
return ApiResponse.success(service.convert(convertScope(user, request.customerId()), request.orderId()));
}
@GetMapping("/{planNo}")
@@ -118,4 +118,12 @@ public class InboundPlanController {
}
return String.valueOf(user.tenantId());
}
private String convertScope(AuthenticatedUser user, String requestedCustomer) {
if (user != null && "PLATFORM".equals(user.appCode()) && user.tenantId() != null && user.tenantId() == 0L
&& (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer.trim()))) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "平台生成入库计划必须指定客户");
}
return scope(user, requestedCustomer);
}
}
@@ -1,9 +1,10 @@
package com.baiyue.lastleg.inboundplan.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public record ConvertInboundPlanRequest(
@NotNull Long orderId,
String customerId
@Size(max = 128) String customerId
) {
}
@@ -59,6 +59,9 @@ public class InboundPlanService {
@Transactional
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
if (customerId == null || customerId.isBlank() || "0".equals(customerId.trim())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "平台生成入库计划必须指定客户");
}
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(customerId, selectedOrderId);
if (orders == null || orders.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound order not found");
@@ -0,0 +1,19 @@
package com.baiyue.lastleg.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDate;
@Mapper
public interface LastlegOrderNoDailySequenceMapper {
@Insert("INSERT INTO ll_order_no_daily_sequence(business_date,last_sequence) " +
"VALUES(#{businessDate},LAST_INSERT_ID(1)) " +
"ON DUPLICATE KEY UPDATE last_sequence=LAST_INSERT_ID(last_sequence+1)")
int nextValue(@Param("businessDate") LocalDate businessDate);
@Select("SELECT LAST_INSERT_ID()")
long currentValue();
}
@@ -1,35 +1,29 @@
package com.baiyue.lastleg.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.mapper.LastlegOrderNoDailySequenceMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@Component
public class LastlegOrderNoGenerator {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
private static final DateTimeFormatter BUSINESS_DATE = DateTimeFormatter.ofPattern("yyMMdd");
@Resource private LastlegOrderNoDailySequenceMapper sequenceMapper;
public String next() {
return "LMO" + IdWorker.getIdStr();
return next(LocalDate.now(BUSINESS_ZONE));
}
/** The physical container number is the tail-leg business order grouping boundary. */
public String forContainerNo(String containerNo) {
if (containerNo == null || containerNo.isBlank()) {
throw new IllegalArgumentException("containerNo is required");
}
String normalized = containerNo.trim();
if (normalized.matches("[0-9]{1,29}")) {
return "LMO" + normalized;
}
try {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(normalized.getBytes(StandardCharsets.UTF_8));
String digits = new BigInteger(1, digest).toString();
return "LMO" + digits.substring(0, Math.min(29, digits.length()));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable", exception);
String next(LocalDate businessDate) {
sequenceMapper.nextValue(businessDate);
long sequence = sequenceMapper.currentValue();
if (sequence < 1 || sequence > 9999) {
throw new IllegalStateException("尾程单日流水超出 0001-9999 范围: " + businessDate);
}
return "LMO-" + BUSINESS_DATE.format(businessDate) + "-" + String.format("%04d", sequence);
}
}
@@ -83,7 +83,7 @@ public class LoadingCompletedConsumer {
shippingGates.initialize(payload.containerBookingId(), envelope.tenantId(), "",
payload.containerNo(), 0, envelope.handoverVersion());
String orderNo = orderNoGenerator.forContainerNo(payload.containerNo());
String orderNo = orderNoGenerator.next();
JsonNode rawOrders = envelope.payload().path("orders");
for (int index = 0; index < payload.orders().size(); index++) {
var source = payload.orders().get(index);
@@ -183,6 +183,7 @@ public class LoadingCompletedConsumer {
order.setInboundCode(source.inboundCode()); order.setSourceRegion(event.sourceRegion().name());
order.setSourceInboundId(source.sourceInboundId()); order.setRootInboundId(source.rootInboundId());
order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId());
order.setCustomerName(source.customerName());
order.setSourceWarehouseId(payload.warehouseId()); order.setSourceWarehouseName(payload.warehouseName());
order.setOverseasWarehouseId(null); order.setOverseasWarehouseName(null);
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(payload.containerNo());
@@ -35,6 +35,12 @@ CREATE TABLE IF NOT EXISTS ll_container_shipping_gate (
KEY idx_ll_shipping_gate_version(container_booking_id,aggregate_version,node_sequence)
) ENGINE=InnoDB COMMENT='海外PDA柜号船节点收货资格快照';
CREATE TABLE IF NOT EXISTS ll_order_no_daily_sequence (
business_date DATE NOT NULL COMMENT '业务日期(Asia/Shanghai)',
last_sequence INT NOT NULL COMMENT '当日已分配尾程单流水号',
PRIMARY KEY(business_date)
) ENGINE=InnoDB COMMENT='尾程单号按日原子流水';
CREATE TABLE IF NOT EXISTS ll_order (
id BIGINT NOT NULL COMMENT '雪花 ID',
order_no VARCHAR(32) NOT NULL COMMENT '尾程单号',
@@ -156,6 +156,22 @@ paths:
- { name: createdFrom, in: query, schema: { type: string, format: date-time } }
- { name: createdTo, in: query, schema: { type: string, format: date-time } }
responses: { '200': { description: 统一 ApiResponse 包装的尾程订单分页;列表行 destinationType 为 FBA 或 OVERSEAS } }
/api/platform/v1/last-leg/inbound-plans/convert:
post:
summary: 平台生成入库计划
description: 权限 platform:overseas-warehouse:inbound-order:create;必须同时传入真实 customerId 与 orderId,服务端按该客户和订单 ID 锁定归属,缺失 customerId 返回 A0400。
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [customerId, orderId]
properties:
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/dispatches:
get:
summary: 分页查询排车单