二期内容提交

This commit is contained in:
2026-08-04 17:18:07 +08:00
parent db72bdf884
commit b7d36d4766
30 changed files with 537 additions and 19 deletions
@@ -18,4 +18,6 @@ public class InboundProperties {
private boolean allowRejectedMeasurement = false;
private BigDecimal defaultPalletCapacityCbm = new BigDecimal("1.200000");
private Duration reservationTtl = Duration.ofMinutes(30);
/** 共享收货仓资源所属租户;0 表示与入库业务租户一致。 */
private long warehouseResourceTenantId = 0L;
}
@@ -0,0 +1,44 @@
package com.baiyue.firstleg.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.service.InboundOrderCreationService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/** 接收订单服务审核通过事件,创建入库单。 */
@RestController
@RequestMapping("/internal/firstleg/v1/inbounds")
public class InternalInboundOrderController {
private final InboundOrderCreationService service;
private final byte[] expectedToken;
public InternalInboundOrderController(InboundOrderCreationService service,
@Value("${baiyue.security.internal-service-token}") String token) {
this.service = service;
this.expectedToken = token.getBytes(StandardCharsets.UTF_8);
}
@PostMapping("/order-approved")
public ApiResponse<Long> createFromApprovedOrder(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@RequestBody OrderApprovedInboundCommand command) {
if (token == null || !MessageDigest.isEqual(expectedToken, token.getBytes(StandardCharsets.UTF_8))) {
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
}
if (command.tenantId() <= 0 || command.orderId() <= 0 || command.warehouseId() == null
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()) {
throw new BizException(ResultCode.BAD_REQUEST, "订单入库快照不完整");
}
return ApiResponse.success(service.createFromApprovedOrder(command));
}
}
@@ -11,6 +11,7 @@ public record InboundOrderSummary(
Long id, Long orderId, String orderNo, String inboundCode,
String processStatus, String orderStatusSnapshot, String measureReviewStatus,
String feeStatus, String feeReviewStatus, String notificationStatus,
Integer version,
LocalDateTime expectedArrivalAt,
Long customerId, String customerName, Long companyId, String companyName,
Long warehouseId, String warehouseName,
@@ -35,6 +36,7 @@ public record InboundOrderSummary(
order.getId(), order.getOrderId(), order.getOrderNo(), order.getInboundCode(),
order.getProcessStatus(), order.getOrderStatusSnapshot(), order.getMeasureReviewStatus(),
order.getFeeStatus(), order.getFeeReviewStatus(), order.getNotificationStatus(),
order.getVersion(),
order.getExpectedArrivalAt(), order.getCustomerId(), order.getCustomerName(),
order.getCompanyId(), order.getCompanyName(), order.getWarehouseId(), order.getWarehouseName(),
order.getDeliveryWarehouseId(), order.getDeliveryWarehouseName(), order.getBusinessType(),
@@ -0,0 +1,33 @@
package com.baiyue.firstleg.dto;
import java.time.LocalDateTime;
/** 订单审核通过后创建入库单的内部服务契约。 */
public record OrderApprovedInboundCommand(
long tenantId,
long orderId,
String orderNo,
Long customerId,
String customerName,
Long warehouseId,
String warehouseName,
Long deliveryWarehouseId,
String deliveryWarehouseName,
LocalDateTime expectedArrivalAt,
String businessType,
String channelCode,
String channelName,
Long carrierId,
String carrierName,
String taxMode,
String containerMode,
Boolean customsInspection,
String destinationType,
Long destinationWarehouseId,
String destinationCode,
String destinationName,
String publicRemark,
String expressCompanyName,
String expressTrackingNo
) {
}
@@ -40,6 +40,8 @@ public class InboundOrderEntity extends BaseEntity {
private String businessType;
private String channelCode;
private String channelName;
private Long carrierId;
private String carrierName;
private String taxMode;
private String containerMode;
private Boolean customsInspection;
@@ -9,6 +9,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Repository
public class InboundPalletAllocationRepository {
@@ -21,6 +24,11 @@ public class InboundPalletAllocationRepository {
@Transactional
public void replace(long tenantId, long inboundId, long warehouseId, BigDecimal capacity,
BatchResult result) {
Map<Long, String> reservationNos = mapper.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
.eq(InboundPalletAllocationEntity::getInboundId, inboundId))
.stream().collect(Collectors.toMap(InboundPalletAllocationEntity::getPalletId,
InboundPalletAllocationEntity::getReservationNo, (left, right) -> left));
mapper.delete(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
.eq(InboundPalletAllocationEntity::getInboundId, inboundId));
@@ -37,7 +45,9 @@ public class InboundPalletAllocationRepository {
value.setAllocatedVolumeCbm(source.allocatedVolumeCbm());
value.setLocationId(source.locationId());
value.setLocationCode(source.locationCode());
value.setReservationNo(source.reservationNo());
// 仓储确认响应只返回 reservationId;沿用预占阶段的预占单号快照。
value.setReservationNo(source.reservationNo() != null ? source.reservationNo()
: reservationNos.get(source.palletId()));
value.setAllocationStatus(normalize(source.state()));
value.setCreatedAt(now);
value.setUpdatedAt(now);
@@ -0,0 +1,89 @@
package com.baiyue.firstleg.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
/** 将已审核的订单快照幂等转换为待测量入库单。 */
@Service
public class InboundOrderCreationService {
private final InboundOrderMapper orders;
private final InboundStatusHistoryMapper histories;
public InboundOrderCreationService(InboundOrderMapper orders, InboundStatusHistoryMapper histories) {
this.orders = orders;
this.histories = histories;
}
@Transactional
public long createFromApprovedOrder(OrderApprovedInboundCommand command) {
InboundOrderEntity existing = orders.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<InboundOrderEntity>()
.eq(InboundOrderEntity::getTenantId, command.tenantId())
.eq(InboundOrderEntity::getOrderId, command.orderId()));
if (existing != null) {
return existing.getId();
}
LocalDateTime now = LocalDateTime.now();
InboundOrderEntity inbound = new InboundOrderEntity();
inbound.setId(IdWorker.getId());
inbound.setTenantId(command.tenantId());
inbound.setOrderId(command.orderId());
inbound.setOrderNo(command.orderNo());
inbound.setInboundCode("RK-" + command.orderId());
inbound.setProcessStatus("WAIT_MEASURE");
inbound.setOrderStatusSnapshot("APPROVED");
inbound.setMeasureReviewStatus("NOT_SUBMITTED");
inbound.setFeeStatus("UNPRICED");
inbound.setFeeReviewStatus("NOT_SUBMITTED");
inbound.setNotificationStatus("NOT_CREATED");
inbound.setExpectedArrivalAt(command.expectedArrivalAt());
inbound.setCustomerId(command.customerId());
inbound.setCustomerName(command.customerName());
inbound.setWarehouseId(command.warehouseId());
inbound.setWarehouseName(command.warehouseName());
inbound.setDeliveryWarehouseId(command.deliveryWarehouseId());
inbound.setDeliveryWarehouseName(command.deliveryWarehouseName());
inbound.setBusinessType(command.businessType());
inbound.setChannelCode(command.channelCode());
inbound.setChannelName(command.channelName());
inbound.setCarrierId(command.carrierId());
inbound.setCarrierName(command.carrierName());
inbound.setTaxMode(command.taxMode());
inbound.setContainerMode(command.containerMode());
inbound.setCustomsInspection(command.customsInspection());
inbound.setDestinationType(command.destinationType());
inbound.setDestinationWarehouseId(command.destinationWarehouseId());
inbound.setDestinationCode(command.destinationCode());
inbound.setDestinationName(command.destinationName());
inbound.setExpressCompanyName(command.expressCompanyName());
inbound.setExpressTrackingNo(command.expressTrackingNo());
inbound.setPublicRemark(command.publicRemark());
inbound.setCreatedBy(0L);
inbound.setUpdatedBy(0L);
inbound.setCreatedAt(now);
inbound.setUpdatedAt(now);
inbound.setDeleted(false);
inbound.setVersion(0);
orders.insert(inbound);
InboundStatusHistoryEntity history = new InboundStatusHistoryEntity();
history.setId(IdWorker.getId());
history.setTenantId(command.tenantId());
history.setInboundId(inbound.getId());
history.setToStatus("WAIT_MEASURE");
history.setAction("ORDER_APPROVED");
history.setOperatorId(0L);
history.setOperatorName("订单服务");
history.setOccurredAt(now);
histories.insert(history);
return inbound.getId();
}
}
@@ -39,10 +39,12 @@ public class MeasurementLocationOccupationService {
int measurementVersion) {
InboundOrderEntity order = requireOrder(tenantId, inboundId);
long warehouseId = requireWarehouse(order);
long resourceTenantId = properties.getWarehouseResourceTenantId() > 0
? properties.getWarehouseResourceTenantId() : tenantId;
BigDecimal volume = requireVolume(order);
String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion;
WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve(
internalToken, new WarehousePalletLocationClient.ReserveRequest(tenantId, warehouseId,
internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId,
SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume,
properties.getDefaultPalletCapacityCbm(), "STANDARD",
LocalDateTime.now().plus(properties.getReservationTtl()), true))),
@@ -51,7 +53,7 @@ public class MeasurementLocationOccupationService {
properties.getDefaultPalletCapacityCbm(), reserved);
try {
WarehousePalletLocationClient.BatchResult occupied = requireSuccess(call(() -> warehouse.confirm(
internalToken, new WarehousePalletLocationClient.ConfirmRequest(tenantId, warehouseId,
internalToken, new WarehousePalletLocationClient.ConfirmRequest(resourceTenantId, warehouseId,
SOURCE_SERVICE, inboundId, operatorId, volume,
properties.getDefaultPalletCapacityCbm()))), "测量完成后库位占用失败");
allocations.replace(tenantId, inboundId, warehouseId,
@@ -59,7 +61,7 @@ public class MeasurementLocationOccupationService {
} catch (RuntimeException failure) {
try {
warehouse.release(internalToken, new WarehousePalletLocationClient.ReleaseRequest(
tenantId, SOURCE_SERVICE, inboundId, operatorId, "测量完成库位占用失败"));
resourceTenantId, SOURCE_SERVICE, inboundId, operatorId, "测量完成库位占用失败"));
} catch (RuntimeException releaseFailure) {
failure.addSuppressed(releaseFailure);
}