二期内容提交
This commit is contained in:
@@ -11,6 +11,7 @@ firstleg:
|
||||
allow-rejected-measurement: false
|
||||
default-pallet-capacity-cbm: ${DEFAULT_PALLET_CAPACITY_CBM:1.200000}
|
||||
reservation-ttl: ${PALLET_RESERVATION_TTL:30m}
|
||||
warehouse-resource-tenant-id: ${WAREHOUSE_RESOURCE_TENANT_ID:0}
|
||||
measurement-openapi:
|
||||
enabled: ${MEASUREMENT_OPENAPI_ENABLED:false}
|
||||
max-clock-skew: ${MEASUREMENT_OPENAPI_MAX_CLOCK_SKEW:5m}
|
||||
|
||||
@@ -315,7 +315,9 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_result (
|
||||
adjusted_at DATETIME(3) NULL COMMENT '修正时间',
|
||||
confirmed_by BIGINT UNSIGNED NULL COMMENT '确认人ID',
|
||||
confirmed_at DATETIME(3) NULL COMMENT '确认时间',
|
||||
created_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+44
@@ -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;
|
||||
|
||||
+11
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -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);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ firstleg:
|
||||
allow-rejected-measurement: false
|
||||
default-pallet-capacity-cbm: ${DEFAULT_PALLET_CAPACITY_CBM:1.200000}
|
||||
reservation-ttl: ${PALLET_RESERVATION_TTL:30m}
|
||||
warehouse-resource-tenant-id: ${WAREHOUSE_RESOURCE_TENANT_ID:0}
|
||||
management.endpoints.web.exposure.include: health,info,prometheus
|
||||
|
||||
logging:
|
||||
|
||||
@@ -223,7 +223,9 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_result (
|
||||
adjusted_at DATETIME(3) NULL COMMENT '修正时间',
|
||||
confirmed_by BIGINT UNSIGNED NULL COMMENT '确认人ID',
|
||||
confirmed_at DATETIME(3) NULL COMMENT '确认时间',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- 二期称重结果审计字段增量迁移(可重复执行)。
|
||||
-- InboundWeightResultEntity 继承 BaseEntity,持久化需要 created_by、updated_by 两个审计字段。
|
||||
SET @has_created_by := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_weight_result' AND column_name = 'created_by'
|
||||
);
|
||||
SET @created_by_sql := IF(@has_created_by = 0,
|
||||
'ALTER TABLE fl_inbound_weight_result ADD COLUMN created_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''创建人ID'' AFTER confirmed_at',
|
||||
'SELECT 1');
|
||||
PREPARE created_by_stmt FROM @created_by_sql;
|
||||
EXECUTE created_by_stmt;
|
||||
DEALLOCATE PREPARE created_by_stmt;
|
||||
|
||||
SET @has_updated_by := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_weight_result' AND column_name = 'updated_by'
|
||||
);
|
||||
SET @updated_by_sql := IF(@has_updated_by = 0,
|
||||
'ALTER TABLE fl_inbound_weight_result ADD COLUMN updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''最后修改人ID'' AFTER created_at',
|
||||
'SELECT 1');
|
||||
PREPARE updated_by_stmt FROM @updated_by_sql;
|
||||
EXECUTE updated_by_stmt;
|
||||
DEALLOCATE PREPARE updated_by_stmt;
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
||||
import com.baiyue.firstleg.config.InboundProperties;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MeasurementLocationOccupationServiceTest {
|
||||
private static final long BUSINESS_TENANT = 10001L;
|
||||
private static final long RESOURCE_TENANT = 90001L;
|
||||
private static final long WAREHOUSE = 200L;
|
||||
private static final long INBOUND = 88L;
|
||||
private static final long OPERATOR = 9L;
|
||||
private static final long MEASUREMENT = 30L;
|
||||
|
||||
private final WarehousePalletLocationClient warehouse = mock(WarehousePalletLocationClient.class);
|
||||
private final InboundPalletAllocationRepository allocations = mock(InboundPalletAllocationRepository.class);
|
||||
private final InboundWorkflowRepository workflow = mock(InboundWorkflowRepository.class);
|
||||
private final InboundProperties properties = properties();
|
||||
private final MeasurementLocationOccupationService service =
|
||||
new MeasurementLocationOccupationService(warehouse, allocations, workflow, properties, "internal-token");
|
||||
|
||||
@Test
|
||||
void sharedWarehouseResourcesUseResourceTenantWhileSnapshotsRemainInBusinessTenant() {
|
||||
properties.setWarehouseResourceTenantId(RESOURCE_TENANT);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("OCCUPIED")));
|
||||
|
||||
service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReserveRequest> reserve =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReserveRequest.class);
|
||||
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
|
||||
assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND);
|
||||
assertThat(reserve.getValue().idempotencyKey()).isEqualTo("measure-88-30-3");
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
|
||||
verify(warehouse).confirm(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
|
||||
assertThat(confirm.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
|
||||
assertThat(confirm.getValue().businessId()).isEqualTo(INBOUND);
|
||||
|
||||
verify(allocations, times(2)).replace(
|
||||
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(INBOUND),
|
||||
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
|
||||
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
|
||||
org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroResourceTenantFallsBackToBusinessTenant() {
|
||||
properties.setWarehouseResourceTenantId(0L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("OCCUPIED")));
|
||||
|
||||
service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReserveRequest> reserve =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReserveRequest.class);
|
||||
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(BUSINESS_TENANT);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
|
||||
verify(warehouse).confirm(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
|
||||
assertThat(confirm.getValue().tenantId()).isEqualTo(BUSINESS_TENANT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmFailureReleasesReservationInResourceTenantAndKeepsOriginalFailure() {
|
||||
properties.setWarehouseResourceTenantId(RESOURCE_TENANT);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
IllegalStateException failure = new IllegalStateException("confirm failed");
|
||||
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenThrow(failure);
|
||||
when(warehouse.release(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(List.of()));
|
||||
|
||||
assertThatThrownBy(() -> service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
|
||||
.isSameAs(failure);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReleaseRequest> release =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReleaseRequest.class);
|
||||
verify(warehouse).release(org.mockito.ArgumentMatchers.eq("internal-token"), release.capture());
|
||||
assertThat(release.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
|
||||
assertThat(release.getValue().businessId()).isEqualTo(INBOUND);
|
||||
verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE,
|
||||
new BigDecimal("1.200000"), batch("RESERVED"));
|
||||
}
|
||||
|
||||
private static InboundProperties properties() {
|
||||
InboundProperties value = new InboundProperties();
|
||||
value.setDefaultPalletCapacityCbm(new BigDecimal("1.200000"));
|
||||
return value;
|
||||
}
|
||||
|
||||
private static InboundOrderEntity inbound() {
|
||||
InboundOrderEntity value = new InboundOrderEntity();
|
||||
value.setTenantId(BUSINESS_TENANT);
|
||||
value.setId(INBOUND);
|
||||
value.setWarehouseId(WAREHOUSE);
|
||||
value.setInboundCode("RK-001");
|
||||
value.setActualVolumeCbm(new BigDecimal("1.0000"));
|
||||
return value;
|
||||
}
|
||||
|
||||
private static WarehousePalletLocationClient.BatchResult batch(String state) {
|
||||
return new WarehousePalletLocationClient.BatchResult(1, List.of(
|
||||
new WarehousePalletLocationClient.AllocationResult(
|
||||
501L, "RES-001", 1, 601L, "PL-001", 701L, "A-001",
|
||||
new BigDecimal("1.0000"), state)));
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 订单微服务启动入口,负责装配订单领域组件并扫描 MyBatis-Plus Mapper。
|
||||
*/
|
||||
@MapperScan("com.baiyue.order.mapper")
|
||||
@EnableFeignClients(basePackages = "com.baiyue.order.client")
|
||||
@EnableScheduling
|
||||
@SpringBootApplication(scanBasePackages = "com.baiyue")
|
||||
public class BaiyueOrderApplication {
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.order.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 订单审核事件投递到头程入库域的内部客户端。 */
|
||||
@FeignClient(name = "firstleg-service", contextId = "orderFirstlegInboundClient",
|
||||
url = "${baiyue.clients.firstleg.url:}", path = "/internal/firstleg/v1/inbounds")
|
||||
public interface FirstlegInboundClient {
|
||||
@PostMapping("/order-approved")
|
||||
ApiResponse<Long> create(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody OrderApprovedInboundCommand command);
|
||||
|
||||
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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,8 @@ public final class OrderRequests {
|
||||
@NotBlank @Size(max = 128) String receivingWarehouseName,
|
||||
@NotBlank @Size(max = 64) String channelCode,
|
||||
@NotBlank @Size(max = 128) String channelName,
|
||||
@NotNull Long carrierId,
|
||||
@NotBlank @Size(max = 128) String carrierName,
|
||||
@NotBlank @Pattern(regexp = "BUY_DECLARATION|TAX_REFUND") String taxMode,
|
||||
@Pattern(regexp = "FULL_CONTAINER|LCL") String containerMode,
|
||||
@NotNull Boolean customsInspection,
|
||||
|
||||
@@ -38,6 +38,8 @@ public class OrderEntity extends BaseEntity {
|
||||
private String receivingTime;
|
||||
private String channelCode;
|
||||
private String channelName;
|
||||
private Long carrierId;
|
||||
private String carrierName;
|
||||
private String taxMode;
|
||||
private String containerMode;
|
||||
private Boolean customsInspection;
|
||||
|
||||
@@ -26,6 +26,8 @@ public class OutboxEventEntity {
|
||||
private String payloadJson;
|
||||
private String publishStatus;
|
||||
private Integer retryCount;
|
||||
private LocalDateTime nextRetryAt;
|
||||
private LocalDateTime publishedAt;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import com.baiyue.order.entity.*;
|
||||
import com.baiyue.order.mapper.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 订单聚合仓储,组合订单主表、货物、参与方、备注、状态历史与 Outbox 的持久化操作,
|
||||
@@ -49,6 +51,29 @@ public class OrderRepository {
|
||||
public void insertNote(OrderInternalNoteEntity note) { noteMapper.insert(note); }
|
||||
public void insertHistory(OrderStatusHistoryEntity history) { historyMapper.insert(history); }
|
||||
public void insertOutbox(OutboxEventEntity event) { outboxMapper.insert(event); }
|
||||
public List<OutboxEventEntity> pendingInboundEvents(int limit) {
|
||||
return outboxMapper.selectList(new LambdaQueryWrapper<OutboxEventEntity>()
|
||||
.eq(OutboxEventEntity::getEventType, "FIRST_LEG_ORDER_APPROVED")
|
||||
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
|
||||
.and(v -> v.isNull(OutboxEventEntity::getNextRetryAt)
|
||||
.or().le(OutboxEventEntity::getNextRetryAt, LocalDateTime.now()))
|
||||
.orderByAsc(OutboxEventEntity::getCreatedAt)
|
||||
.last("LIMIT " + Math.min(Math.max(limit, 1), 100)));
|
||||
}
|
||||
public void markOutboxPublished(long id, LocalDateTime publishedAt) {
|
||||
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
|
||||
.eq(OutboxEventEntity::getId, id)
|
||||
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
|
||||
.set(OutboxEventEntity::getPublishStatus, "PUBLISHED")
|
||||
.set(OutboxEventEntity::getPublishedAt, publishedAt));
|
||||
}
|
||||
public void markOutboxRetry(long id, LocalDateTime nextRetryAt) {
|
||||
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
|
||||
.eq(OutboxEventEntity::getId, id)
|
||||
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
|
||||
.setSql("retry_count = retry_count + 1")
|
||||
.set(OutboxEventEntity::getNextRetryAt, nextRetryAt));
|
||||
}
|
||||
public void insertIdempotency(OrderIdempotencyRecordEntity record) { idempotencyMapper.insert(record); }
|
||||
|
||||
public OrderIdempotencyRecordEntity findIdempotency(
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.dto.OrderDetailResponse;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.dto.TenantOrderCountResponse;
|
||||
import com.baiyue.order.client.FirstlegInboundClient;
|
||||
import com.baiyue.order.entity.*;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
@@ -304,7 +305,9 @@ public class OrderApplicationService {
|
||||
order.setReceivingWarehouseAddress(formatAddress(snapshots.receiving()));
|
||||
order.setReceivingTime(formatReceivingTime(snapshots.receiving()));
|
||||
order.setChannelCode(request.channelCode());
|
||||
order.setChannelName(request.channelName()); order.setTaxMode(request.taxMode());
|
||||
order.setChannelName(request.channelName());
|
||||
order.setCarrierId(request.carrierId()); order.setCarrierName(request.carrierName());
|
||||
order.setTaxMode(request.taxMode());
|
||||
order.setContainerMode(request.containerMode()); order.setCustomsInspection(request.customsInspection());
|
||||
order.setDestinationType(request.destinationType());
|
||||
order.setAmazonWarehouseCode("FBA".equals(request.destinationType()) && snapshots.destination() != null
|
||||
@@ -366,12 +369,26 @@ public class OrderApplicationService {
|
||||
event.setTenantId(order.getTenantId()); event.setAggregateType("FIRST_LEG_ORDER"); event.setAggregateId(order.getId());
|
||||
event.setEventType(target == OrderStatus.APPROVED ? "FIRST_LEG_ORDER_APPROVED" : "FIRST_LEG_ORDER_CANCELLED");
|
||||
try {
|
||||
event.setPayloadJson(json.writeValueAsString(Map.of("tenantId", order.getTenantId(), "orderId", order.getId(),
|
||||
event.setPayloadJson(target == OrderStatus.APPROVED
|
||||
? json.writeValueAsString(inboundCommand(order))
|
||||
: json.writeValueAsString(Map.of("tenantId", order.getTenantId(), "orderId", order.getId(),
|
||||
"orderNo", order.getOrderNo(), "status", target.name())));
|
||||
} catch (JsonProcessingException exception) { throw new BizException("Cannot serialize order event"); }
|
||||
event.setPublishStatus("PENDING"); event.setRetryCount(0); event.setCreatedAt(LocalDateTime.now());
|
||||
repository.insertOutbox(event);
|
||||
}
|
||||
private FirstlegInboundClient.OrderApprovedInboundCommand inboundCommand(OrderEntity order) {
|
||||
return new FirstlegInboundClient.OrderApprovedInboundCommand(
|
||||
order.getTenantId(), order.getId(), order.getOrderNo(), order.getCustomerId(), order.getCustomerName(),
|
||||
order.getReceivingWarehouseId(), order.getReceivingWarehouseName(), order.getDestinationWarehouseId(),
|
||||
order.getDestinationWarehouseName(), order.getExpectedShipDate() == null ? null : order.getExpectedShipDate().atStartOfDay(),
|
||||
order.getDestinationType() + "_" + order.getTransportMode(), order.getChannelCode(), order.getChannelName(),
|
||||
order.getCarrierId(), order.getCarrierName(),
|
||||
"BUY_DECLARATION".equals(order.getTaxMode()) ? "TAX_INCLUDED" : "TAX_EXCLUDED",
|
||||
order.getContainerMode(), order.getCustomsInspection(), order.getDestinationType(),
|
||||
order.getDestinationWarehouseId(), order.getAmazonWarehouseCode(), order.getDestinationWarehouseName(),
|
||||
order.getCustomerRemark(), order.getExpressCompany(), order.getExpressTrackingNo());
|
||||
}
|
||||
private String nextNo(long orderId) {
|
||||
return "FL" + orderId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.order.client.FirstlegInboundClient;
|
||||
import com.baiyue.order.entity.OutboxEventEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 可靠投递订单审核事件;接收端以订单唯一键保证重复投递安全。 */
|
||||
@Component
|
||||
public class OrderInboundOutboxPublisher {
|
||||
private final OrderRepository repository;
|
||||
private final FirstlegInboundClient client;
|
||||
private final ObjectMapper json;
|
||||
private final String internalToken;
|
||||
|
||||
public OrderInboundOutboxPublisher(OrderRepository repository, FirstlegInboundClient client,
|
||||
ObjectMapper json,
|
||||
@Value("${baiyue.security.internal-service-token}") String internalToken) {
|
||||
this.repository = repository;
|
||||
this.client = client;
|
||||
this.json = json;
|
||||
this.internalToken = internalToken;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${baiyue.order.inbound-outbox.fixed-delay-ms:3000}")
|
||||
public void publishApprovedOrders() {
|
||||
for (OutboxEventEntity event : repository.pendingInboundEvents(50)) {
|
||||
try {
|
||||
FirstlegInboundClient.OrderApprovedInboundCommand command = json.readValue(
|
||||
event.getPayloadJson(), FirstlegInboundClient.OrderApprovedInboundCommand.class);
|
||||
ApiResponse<Long> response = client.create(internalToken, command);
|
||||
if (response == null || !response.successful()) {
|
||||
throw new IllegalStateException("头程入库服务返回失败");
|
||||
}
|
||||
repository.markOutboxPublished(event.getId(), LocalDateTime.now());
|
||||
} catch (Exception exception) {
|
||||
repository.markOutboxRetry(event.getId(), LocalDateTime.now().plusSeconds(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
receiving_time VARCHAR(128) NULL COMMENT '国内收货仓收货时间快照',
|
||||
channel_code VARCHAR(64) NULL COMMENT '渠道编码',
|
||||
channel_name VARCHAR(128) NULL COMMENT '渠道名称快照',
|
||||
carrier_id BIGINT UNSIGNED NULL COMMENT '船司逻辑ID快照',
|
||||
carrier_name VARCHAR(128) NULL COMMENT '船司名称快照',
|
||||
tax_mode VARCHAR(24) NULL COMMENT '买单或退税',
|
||||
container_mode VARCHAR(24) NULL COMMENT '整柜或散货',
|
||||
customs_inspection TINYINT(1) NULL COMMENT '是否商检',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_order;
|
||||
|
||||
SET @carrier_id_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ord_order' AND column_name = 'carrier_id'
|
||||
);
|
||||
SET @carrier_id_sql := IF(@carrier_id_exists = 0,
|
||||
'ALTER TABLE ord_order ADD COLUMN carrier_id BIGINT UNSIGNED NULL COMMENT ''船司逻辑ID快照'' AFTER channel_name',
|
||||
'SELECT 1');
|
||||
PREPARE carrier_id_statement FROM @carrier_id_sql;
|
||||
EXECUTE carrier_id_statement;
|
||||
DEALLOCATE PREPARE carrier_id_statement;
|
||||
|
||||
SET @carrier_name_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ord_order' AND column_name = 'carrier_name'
|
||||
);
|
||||
SET @carrier_name_sql := IF(@carrier_name_exists = 0,
|
||||
'ALTER TABLE ord_order ADD COLUMN carrier_name VARCHAR(128) NULL COMMENT ''船司名称快照'' AFTER carrier_id',
|
||||
'SELECT 1');
|
||||
PREPARE carrier_name_statement FROM @carrier_name_sql;
|
||||
EXECUTE carrier_name_statement;
|
||||
DEALLOCATE PREPARE carrier_name_statement;
|
||||
@@ -180,7 +180,7 @@ class OrderApplicationServiceStateTest {
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
return new OrderRequests.SaveRequest(
|
||||
7L, "客户", "SEA", null, "US", null, "目的仓", null, null,
|
||||
party, party, List.of(cargo), "PICKUP", 8L, "收货仓", "CH", "渠道",
|
||||
party, party, List.of(cargo), "PICKUP", 8L, "收货仓", "CH", "渠道", 9L, "船司",
|
||||
"BUY_DECLARATION", "LCL", false, "OVERSEAS", null, null, null);
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ class OrderApplicationServiceStateTest {
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
return new OrderRequests.SaveRequest(
|
||||
customerId, "当前企业", "SEA", null, "US", null, "目的仓", null, null,
|
||||
sender, receiver, List.of(cargo), pickupMethod, 8L, "收货仓", "CH", "渠道",
|
||||
sender, receiver, List.of(cargo), pickupMethod, 8L, "收货仓", "CH", "渠道", 9L, "船司",
|
||||
"BUY_DECLARATION", "LCL", false, destinationType, null, null, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ class OrderMasterDataGuardTest {
|
||||
var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null);
|
||||
var cargo=new OrderRequests.Cargo("货物",null,"CARTON",1,BigDecimal.ONE,BigDecimal.ONE,
|
||||
null,null,null,null,null,false,null,null,null,null,null,null,null,null,null,null,null,null,null);
|
||||
return new OrderRequests.SaveRequest(partner,"不可信客户名","SEA",null,"CN",destinationWarehouseId,"私人地址",null,null,party,party,List.of(cargo),"PICKUP",warehouse,"不可信收货仓","CH","渠道","BUY_DECLARATION","LCL",false,destinationType,null,null,null);
|
||||
return new OrderRequests.SaveRequest(partner,"不可信客户名","SEA",null,"CN",destinationWarehouseId,"私人地址",null,null,party,party,List.of(cargo),"PICKUP",warehouse,"不可信收货仓","CH","渠道",9L,"船司","BUY_DECLARATION","LCL",false,destinationType,null,null,null);
|
||||
}
|
||||
private WarehouseClient.Detail warehouse(long id,String type,boolean receiving) {
|
||||
var value=new WarehouseClient.Warehouse(id,PUBLIC_TENANT_ID,"WH-"+id,"仓库"+id,type,"CN","ACTIVE");
|
||||
|
||||
@@ -5,6 +5,8 @@ param(
|
||||
[string]$OrderUrl = 'http://127.0.0.1:9504',
|
||||
[string]$WarehouseUrl = 'http://127.0.0.1:9505',
|
||||
[string]$ImagePath,
|
||||
[long]$PublicReceivingWarehouseId = 0,
|
||||
[long]$PublicDestinationWarehouseId = 0,
|
||||
[string]$PlatformPassword = 'ChangeMe123!'
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -62,13 +64,14 @@ $warehouse = Invoke-Json POST "$WarehouseUrl/api/platform/v1/warehouse-admin" @{
|
||||
} $platformToken
|
||||
Assert-Success 'create tenant receiving warehouse' $warehouse
|
||||
$warehouseId = [long]$warehouse.data
|
||||
if ($PublicReceivingWarehouseId -gt 0) { $warehouseId = $PublicReceivingWarehouseId }
|
||||
|
||||
$order = @{
|
||||
customerId=$partnerId; customerName="Phase One Partner $stamp"; transportMode='SEA'; tradeTerm='FOB'
|
||||
destinationCountryCode='US'; destinationWarehouseId=$null; destinationWarehouseName='ONT8'
|
||||
destinationCountryCode='US'; destinationWarehouseId=$(if ($PublicDestinationWarehouseId -gt 0) {$PublicDestinationWarehouseId} else {$null}); destinationWarehouseName='ONT8'
|
||||
expectedShipDate='2026-08-15'; customerRemark='Phase one E2E'; pickupMethod='PICKUP'
|
||||
receivingWarehouseId=$warehouseId; receivingWarehouseName="Phase One Receiving Warehouse $stamp"; channelCode='SEA-US-FBA'
|
||||
channelName='US Sea Channel'; taxMode='BUY_DECLARATION'; containerMode='LCL'; customsInspection=$false
|
||||
channelName='US Sea Channel'; carrierId=9900001; carrierName='模拟海运船司'; taxMode='BUY_DECLARATION'; containerMode='LCL'; customsInspection=$false
|
||||
destinationType='FBA'; amazonWarehouseCode='ONT8'; shipmentId="FBA$stamp"; referenceId="REF$stamp"
|
||||
sender=@{companyName="Phase One Enterprise $stamp";contactName='Test Sender';phone='13800138000';email="p$stamp@example.com";countryCode='CN';province='Guangdong';city='Shenzhen';district='Nanshan';addressLine='No. 1 Test Road';postalCode='518000'}
|
||||
receiver=@{companyName='Amazon';contactName='ONT8';phone='1234567890';email='ont8@example.com';countryCode='US';province='CA';city='Moreno Valley';district='Riverside';addressLine='24300 Nandina Ave';postalCode='92551'}
|
||||
|
||||
@@ -140,6 +140,7 @@ public final class LocationDtos {
|
||||
|
||||
public record ActiveResource(
|
||||
long reservationId,
|
||||
String reservationNo,
|
||||
Long occupancyId,
|
||||
long warehouseId,
|
||||
long palletId,
|
||||
@@ -149,5 +150,11 @@ public final class LocationDtos {
|
||||
boolean sharedOverflow,
|
||||
String state
|
||||
) {
|
||||
public ActiveResource(long reservationId, Long occupancyId, long warehouseId, long palletId,
|
||||
String palletCode, long locationId, String locationCode,
|
||||
boolean sharedOverflow, String state) {
|
||||
this(reservationId, null, occupancyId, warehouseId, palletId, palletCode, locationId,
|
||||
locationCode, sharedOverflow, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -163,7 +165,7 @@ public class LocationResourceService {
|
||||
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "释放占用时库位状态已被并发修改");
|
||||
}
|
||||
released.add(active(occupancy, "RELEASED"));
|
||||
released.add(active(occupancy, "RELEASED", null));
|
||||
}
|
||||
for (LocationReservationEntity reservation : repository.findReservationsByBusiness(
|
||||
command.tenantId(), command.sourceService(), command.businessId())) {
|
||||
@@ -187,13 +189,17 @@ public class LocationResourceService {
|
||||
@NotBlank String sourceService,
|
||||
@Min(1) long businessId) {
|
||||
List<ActiveResource> result = new ArrayList<>();
|
||||
repository.findReservationsByBusiness(tenantId, sourceService, businessId).stream()
|
||||
List<LocationReservationEntity> reservations = repository.findReservationsByBusiness(
|
||||
tenantId, sourceService, businessId);
|
||||
Map<Long, String> reservationNos = reservations.stream().collect(Collectors.toMap(
|
||||
LocationReservationEntity::getId, LocationReservationEntity::getReservationNo));
|
||||
reservations.stream()
|
||||
.filter(item -> "RESERVED".equals(item.getReservationStatus()))
|
||||
.map(item -> active(item, "RESERVED"))
|
||||
.forEach(result::add);
|
||||
repository.findOccupanciesByBusiness(tenantId, sourceService, businessId).stream()
|
||||
.filter(item -> "OCCUPIED".equals(item.getOccupancyStatus()))
|
||||
.map(item -> active(item, "OCCUPIED"))
|
||||
.map(item -> active(item, "OCCUPIED", reservationNos.get(item.getReservationId())))
|
||||
.forEach(result::add);
|
||||
return List.copyOf(result);
|
||||
}
|
||||
@@ -326,13 +332,13 @@ public class LocationResourceService {
|
||||
}
|
||||
|
||||
private static ActiveResource active(LocationReservationEntity entity, String state) {
|
||||
return new ActiveResource(entity.getId(), null, entity.getWarehouseId(), entity.getPalletId(),
|
||||
return new ActiveResource(entity.getId(), entity.getReservationNo(), null, entity.getWarehouseId(), entity.getPalletId(),
|
||||
entity.getPalletCode(), entity.getLocationId(), entity.getLocationCode(),
|
||||
Boolean.TRUE.equals(entity.getSharedOverflow()), state);
|
||||
}
|
||||
|
||||
private static ActiveResource active(LocationOccupancyEntity entity, String state) {
|
||||
return new ActiveResource(entity.getReservationId(), entity.getId(), entity.getWarehouseId(),
|
||||
private static ActiveResource active(LocationOccupancyEntity entity, String state, String reservationNo) {
|
||||
return new ActiveResource(entity.getReservationId(), reservationNo, entity.getId(), entity.getWarehouseId(),
|
||||
entity.getPalletId(), entity.getPalletCode(), entity.getLocationId(), entity.getLocationCode(),
|
||||
Boolean.TRUE.equals(entity.getSharedOverflow()), state);
|
||||
}
|
||||
|
||||
+2
-2
@@ -107,7 +107,7 @@ public class PalletLocationAllocationService {
|
||||
List<PalletAllocationDtos.AllocationResult> results = new ArrayList<>(occupied.size());
|
||||
for (int index = 0; index < occupied.size(); index++) {
|
||||
var item = occupied.get(index);
|
||||
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), null, index + 1,
|
||||
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), active.get(index).reservationNo(), index + 1,
|
||||
item.palletId(), active.get(index).palletCode(), item.locationId(), item.locationCode(),
|
||||
split.allocations().get(index).allocatedVolumeCbm(), "OCCUPIED"));
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public class PalletLocationAllocationService {
|
||||
List<PalletAllocationDtos.AllocationResult> results = new ArrayList<>(active.size());
|
||||
for (int index = 0; index < active.size(); index++) {
|
||||
var item = active.get(index);
|
||||
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), null, index + 1,
|
||||
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), item.reservationNo(), index + 1,
|
||||
item.palletId(), item.palletCode(), item.locationId(), item.locationCode(),
|
||||
split.allocations().get(index).allocatedVolumeCbm(), item.state()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user