This commit is contained in:
2026-08-06 15:32:11 +08:00
parent f3cfbdf59b
commit 5f8b1f64da
21 changed files with 435 additions and 36 deletions
@@ -1828,6 +1828,8 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
pallet_type VARCHAR(32) NOT NULL COMMENT '托盘类型',
pallet_name VARCHAR(100) NULL COMMENT '托盘名称',
capacity_cbm DECIMAL(18,6) NOT NULL COMMENT '托盘可用容积CBM',
weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '托盘重量kg',
color VARCHAR(32) NOT NULL DEFAULT '' COMMENT '托盘颜色',
status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE' COMMENT '内部状态:AVAILABLE、RESERVED或OCCUPIED',
remark VARCHAR(500) NULL COMMENT '备注',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
@@ -1837,13 +1839,23 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id),
UNIQUE KEY uk_pallet_tenant_warehouse_code (tenant_id, warehouse_id, pallet_code),
UNIQUE KEY uk_pallet_tenant_code (tenant_id, pallet_code),
KEY idx_pallet_available (tenant_id, warehouse_id, status, deleted),
KEY idx_pallet_type (tenant_id, warehouse_id, pallet_type, status, deleted),
CONSTRAINT fk_pallet_warehouse FOREIGN KEY (warehouse_id) REFERENCES wh_warehouse (id),
CONSTRAINT ck_pallet_capacity CHECK (capacity_cbm > 0)
) ENGINE=InnoDB COMMENT='二期托盘基础资料和物理托盘实例';
-- 存量库补齐托盘重量、颜色字段(可重复执行)。
SET @pallet_weight_sql = IF((SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet' AND column_name = 'weight_kg') = 0,
'ALTER TABLE wh_pallet ADD COLUMN weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''托盘重量kg'' AFTER capacity_cbm', 'SELECT 1');
PREPARE pallet_weight_stmt FROM @pallet_weight_sql; EXECUTE pallet_weight_stmt; DEALLOCATE PREPARE pallet_weight_stmt;
SET @pallet_color_sql = IF((SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet' AND column_name = 'color') = 0,
'ALTER TABLE wh_pallet ADD COLUMN color VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''托盘颜色'' AFTER weight_kg', 'SELECT 1');
PREPARE pallet_color_stmt FROM @pallet_color_sql; EXECUTE pallet_color_stmt; DEALLOCATE PREPARE pallet_color_stmt;
CREATE TABLE IF NOT EXISTS wh_location (
id BIGINT UNSIGNED NOT NULL COMMENT '库位ID(雪花ID)',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共库位固定使用 tenant_id=0',
@@ -2112,6 +2124,27 @@ UPDATE wh_location SET tenant_id = 0 WHERE tenant_id <> 0;
UPDATE wh_location_reservation SET tenant_id = 0 WHERE tenant_id <> 0;
UPDATE wh_location_occupancy SET tenant_id = 0 WHERE tenant_id <> 0;
-- 托盘编码在平台公共作用域全局唯一;包含逻辑删除记录,编码不复用。
DROP PROCEDURE IF EXISTS assert_pallet_code_globally_unique;
DELIMITER $$
CREATE PROCEDURE assert_pallet_code_globally_unique()
BEGIN
IF EXISTS (SELECT 1 FROM wh_pallet GROUP BY tenant_id, pallet_code HAVING COUNT(*) > 1) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '托盘编码迁移失败:不同仓库存在重复编码,请先清理';
END IF;
END$$
DELIMITER ;
CALL assert_pallet_code_globally_unique();
DROP PROCEDURE IF EXISTS assert_pallet_code_globally_unique;
SET @pallet_code_index_sql = IF((SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet' AND index_name = 'uk_pallet_tenant_warehouse_code') > 0,
'ALTER TABLE wh_pallet DROP INDEX uk_pallet_tenant_warehouse_code', 'SELECT 1');
PREPARE pallet_code_index_stmt FROM @pallet_code_index_sql; EXECUTE pallet_code_index_stmt; DEALLOCATE PREPARE pallet_code_index_stmt;
SET @pallet_code_index_sql = IF((SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet' AND index_name = 'uk_pallet_tenant_code') = 0,
'ALTER TABLE wh_pallet ADD UNIQUE KEY uk_pallet_tenant_code (tenant_id, pallet_code)', 'SELECT 1');
PREPARE pallet_code_index_stmt FROM @pallet_code_index_sql; EXECUTE pallet_code_index_stmt; DEALLOCATE PREPARE pallet_code_index_stmt;
DROP PROCEDURE IF EXISTS assert_phase2_public_resource_scope;
DELIMITER $$
CREATE PROCEDURE assert_phase2_public_resource_scope()
@@ -3928,6 +3961,23 @@ JOIN wh_warehouse warehouse
ON DUPLICATE KEY UPDATE
tenant_id = VALUES(tenant_id);
-- 平台托盘初始化主数据;内部状态仅供预占/占用,不作为平台维护字段。
INSERT INTO wh_pallet (
id, tenant_id, warehouse_id, pallet_code, pallet_type, capacity_cbm,
weight_kg, color, status, remark, created_by, updated_by
)
VALUES
(193000000000200101,0,193000000000200001,'TP-001','木托盘',1.200000,8.000,'黄色','AVAILABLE',NULL,0,0),
(193000000000200102,0,193000000000200001,'TP-002','胶托盘',2.400000,12.000,'黑色','AVAILABLE',NULL,0,0)
ON DUPLICATE KEY UPDATE
tenant_id = 0,
warehouse_id = VALUES(warehouse_id),
pallet_type = VALUES(pallet_type),
capacity_cbm = VALUES(capacity_cbm),
weight_kg = VALUES(weight_kg),
color = VALUES(color),
remark = VALUES(remark);
DROP TEMPORARY TABLE IF EXISTS tmp_wh_fba_public_master_data;
CREATE TEMPORARY TABLE tmp_wh_fba_public_master_data (
warehouse_id_seed BIGINT UNSIGNED NOT NULL,
@@ -4936,4 +4986,4 @@ SELECT COUNT(*) AS unresolved_third_party_calls
WHERE tenant_id = 0;
SELECT 'PASS' AS migration_status,
'统一数据库脚本执行完成,三期出仓、费用、公共资源及可靠审计结构校验通过' AS message;
'统一增量全部执行完成;统一数据库脚本执行完成,三期出仓、费用、公共资源及可靠审计结构校验通过' AS message;
@@ -4,7 +4,9 @@ import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/** Order service internal query for the product-level order line. */
public record UnifiedInboundQuery(
@@ -15,6 +17,18 @@ public record UnifiedInboundQuery(
Long customerId,
Long receivingWarehouseId,
Long destinationWarehouseId,
@Size(max = 64) String inboundCode,
@Size(max = 32) String internalOrderStatus,
LocalDate expectedArrivalDate,
@Size(max = 32) String feeReviewStatus,
@Size(max = 128) String channelName,
@Size(max = 64) String containerNo,
@Size(max = 64) String cabinetNo,
LocalDate sailingDate,
Boolean loaded,
Boolean fullContainer,
@Size(max = 32) String measurementReviewStatus,
List<Long> candidateOrderIds,
LocalDateTime createdFrom,
LocalDateTime createdTo,
@Min(1) long page,
@@ -110,6 +110,9 @@ public class InboundOrderRepository {
.eq(query.tenantId() != null, InboundOrderEntity::getTenantId, query.tenantId())
.in(processStatuses != null && !processStatuses.isEmpty(), InboundOrderEntity::getProcessStatus, processStatuses)
.like(StringUtils.hasText(query.orderNo()), InboundOrderEntity::getOrderNo, query.orderNo())
.like(StringUtils.hasText(query.inboundCode()), InboundOrderEntity::getInboundCode, query.inboundCode())
.eq(StringUtils.hasText(query.internalOrderStatus()), InboundOrderEntity::getOrderStatusSnapshot,
query.internalOrderStatus())
.and(StringUtils.hasText(query.keyword()), nested -> nested
.like(InboundOrderEntity::getOrderNo, query.keyword())
.or().like(InboundOrderEntity::getInboundCode, query.keyword())
@@ -117,6 +120,57 @@ public class InboundOrderRepository {
.eq(query.customerId() != null, InboundOrderEntity::getCustomerId, query.customerId())
.eq(query.receivingWarehouseId() != null, InboundOrderEntity::getWarehouseId, query.receivingWarehouseId())
.eq(query.destinationWarehouseId() != null, InboundOrderEntity::getDeliveryWarehouseId, query.destinationWarehouseId())
.ge(query.expectedArrivalDate() != null, InboundOrderEntity::getExpectedArrivalAt,
query.expectedArrivalDate() == null ? null : query.expectedArrivalDate().atStartOfDay())
.lt(query.expectedArrivalDate() != null, InboundOrderEntity::getExpectedArrivalAt,
query.expectedArrivalDate() == null ? null : query.expectedArrivalDate().plusDays(1).atStartOfDay())
.eq(StringUtils.hasText(query.channelName()), InboundOrderEntity::getChannelName, query.channelName())
.eq(query.fullContainer() != null, InboundOrderEntity::getContainerMode,
query.fullContainer() == null ? null : query.fullContainer() ? "FULL_CONTAINER" : "LCL")
.and("UNRECORDED".equals(query.measurementReviewStatus()), nested -> nested
.isNull(InboundOrderEntity::getMeasureReviewStatus)
.or().eq(InboundOrderEntity::getMeasureReviewStatus, ""))
.eq(StringUtils.hasText(query.measurementReviewStatus())
&& !"UNRECORDED".equals(query.measurementReviewStatus()),
InboundOrderEntity::getMeasureReviewStatus, query.measurementReviewStatus())
.in(query.candidateOrderIds() != null && !query.candidateOrderIds().isEmpty(),
InboundOrderEntity::getOrderId,
query.candidateOrderIds() == null ? List.of() : query.candidateOrderIds())
.apply("UNRECORDED".equals(query.feeReviewStatus()),
"NOT EXISTS (SELECT 1 FROM fl_inbound_fee f WHERE f.tenant_id = fl_inbound_order.tenant_id "
+ "AND f.inbound_id = fl_inbound_order.id)")
.apply(StringUtils.hasText(query.feeReviewStatus()) && !"UNRECORDED".equals(query.feeReviewStatus()),
"EXISTS (SELECT 1 FROM fl_inbound_fee f WHERE f.tenant_id = fl_inbound_order.tenant_id "
+ "AND f.inbound_id = fl_inbound_order.id AND f.review_status = {0} "
+ "AND NOT EXISTS (SELECT 1 FROM fl_inbound_fee newer "
+ "WHERE newer.tenant_id = f.tenant_id AND newer.inbound_id = f.inbound_id "
+ "AND (newer.fee_version > f.fee_version "
+ "OR (newer.fee_version = f.fee_version AND newer.id > f.id))))",
query.feeReviewStatus())
.apply(StringUtils.hasText(query.containerNo()),
"EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
+ "AND t.container_no LIKE CONCAT('%', {0}, '%'))", query.containerNo())
.apply(StringUtils.hasText(query.cabinetNo()),
"EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
+ "AND t.cabinet_no LIKE CONCAT('%', {0}, '%'))", query.cabinetNo())
.apply(query.sailingDate() != null,
"EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
+ "AND t.sailing_at >= {0} AND t.sailing_at < {1})",
query.sailingDate() == null ? null : query.sailingDate().atStartOfDay(),
query.sailingDate() == null ? null : query.sailingDate().plusDays(1).atStartOfDay())
.apply(Boolean.TRUE.equals(query.loaded()),
"EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
+ "AND COALESCE(TRIM(t.loading_status), '') <> '' "
+ "AND t.loading_status NOT IN ('UNLOADED', 'PENDING'))")
.apply(Boolean.FALSE.equals(query.loaded()),
"NOT EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
+ "AND COALESCE(TRIM(t.loading_status), '') <> '' "
+ "AND t.loading_status NOT IN ('UNLOADED', 'PENDING'))")
.ge(query.createdFrom() != null, InboundOrderEntity::getCreatedAt, query.createdFrom())
.le(query.createdTo() != null, InboundOrderEntity::getCreatedAt, query.createdTo())
.orderByDesc(InboundOrderEntity::getCreatedAt).orderByDesc(InboundOrderEntity::getId);
@@ -36,7 +36,8 @@ public class UnifiedInboundQueryService {
public PageResult<UnifiedInboundSummary> page(UnifiedInboundQuery query) {
validate(query);
if ("PENDING_REVIEW".equals(query.mainStatus())) {
if ("PENDING_REVIEW".equals(query.mainStatus())
|| query.candidateOrderIds() != null && query.candidateOrderIds().isEmpty()) {
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
}
Page<InboundOrderEntity> source = inbounds.pageUnifiedEntities(query, internalStatuses(query.mainStatus()));
@@ -53,7 +53,9 @@ class InternalInboundOrderReferenceControllerTest {
@Test
void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() {
UnifiedInboundQuery request = new UnifiedInboundQuery(
10001L, null, null, "WAIT_SPLIT", null, null, null, null, null, 1, 20);
10001L, null, null, "WAIT_SPLIT", null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, 1, 20);
when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
assertThat(controller.unifiedPage("internal-token", request).data().records()).isEmpty();
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.firstleg.dto.InboundOrderQuery;
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
@@ -29,6 +30,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
import java.util.List;
@SpringJUnitConfig(InboundOrderRepository.class)
class InboundOrderRepositoryTest {
@MockBean private InboundOrderMapper orders;
@@ -38,6 +42,34 @@ class InboundOrderRepositoryTest {
@MockBean private InboundInternalNoteMapper notes;
@MockBean private InboundStatusHistoryMapper histories;
@Resource private InboundOrderRepository repository;
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void unifiedQueryPushesExpandedFiltersIntoDatabasePage() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), InboundOrderEntity.class);
when(orders.selectPage(any(Page.class), any(Wrapper.class)))
.thenReturn(new Page<InboundOrderEntity>(1, 20));
UnifiedInboundQuery query = new UnifiedInboundQuery(
10001L, null, null, "LOADING", 30001L, 40001L, 50001L,
"RK-", "APPROVED", LocalDate.of(2026, 8, 6), "APPROVED", "美森",
"MSCU", "CAB", LocalDate.of(2026, 8, 8), true, true, "UNRECORDED",
List.of(77L, 78L), null, null, 1, 20);
repository.pageUnifiedEntities(query, List.of("LOADING"));
ArgumentCaptor<Wrapper<InboundOrderEntity>> captor = ArgumentCaptor.forClass(Wrapper.class);
verify(orders).selectPage(any(Page.class), captor.capture());
String sql = captor.getValue().getCustomSqlSegment();
assertThat(sql).contains("inbound_code", "order_status_snapshot", "expected_arrival_at",
"channel_name", "container_mode", "measure_review_status", "order_id IN",
"fl_inbound_fee", "fl_inbound_transport_snapshot", "sailing_at", "loading_status");
LambdaQueryWrapper<InboundOrderEntity> wrapper =
(LambdaQueryWrapper<InboundOrderEntity>) captor.getValue();
assertThat(wrapper.getParamNameValuePairs().values())
.contains(10001L, 30001L, 40001L, 50001L, "%RK-%", "APPROVED", "美森",
"MSCU", "CAB", 77L, 78L);
}
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void shouldApplyTenantCompanyAndWarehouseFilters() {
@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@@ -29,6 +30,10 @@ public interface FirstlegUnifiedOrderClient {
record Query(Long tenantId, String keyword, String orderNo, String mainStatus, Long customerId,
Long receivingWarehouseId, Long destinationWarehouseId,
String inboundCode, String internalOrderStatus, LocalDate expectedArrivalDate,
String feeReviewStatus, String channelName, String containerNo, String cabinetNo,
LocalDate sailingDate, Boolean loaded, Boolean fullContainer,
String measurementReviewStatus, List<Long> candidateOrderIds,
LocalDateTime createdFrom, LocalDateTime createdTo, long page, long size) {}
record BatchRequest(Long tenantId, List<Long> orderIds) {}
record DetailRequest(long tenantId, long orderId) {}
@@ -5,6 +5,7 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.time.LocalDateTime;
/** Unified product order list filters shared by platform and merchant entry points. */
@@ -17,6 +18,18 @@ public record UnifiedOrderQuery(
Long customerId,
Long receivingWarehouseId,
Long destinationWarehouseId,
@Size(max = 64) String inboundCode,
@Size(max = 32) String internalOrderStatus,
LocalDate expectedArrivalDate,
@Size(max = 32) String feeReviewStatus,
@Size(max = 128) String channelName,
@Size(max = 64) String containerNo,
@Size(max = 64) String cabinetNo,
LocalDate sailingDate,
Boolean loaded,
Boolean fullContainer,
@Size(max = 32) String measurementReviewStatus,
@Size(max = 64) String fbaCode,
LocalDateTime createdFrom,
LocalDateTime createdTo,
@Min(1) long page,
@@ -166,6 +166,15 @@ public class OrderRepository {
query.receivingWarehouseId())
.eq(query.destinationWarehouseId() != null, OrderEntity::getDestinationWarehouseId,
query.destinationWarehouseId())
.eq(StringUtils.hasText(query.internalOrderStatus()), OrderEntity::getStatus,
StringUtils.hasText(query.internalOrderStatus()) ? OrderStatus.valueOf(query.internalOrderStatus()) : null)
.eq(StringUtils.hasText(query.channelName()), OrderEntity::getChannelName, query.channelName())
.eq(query.fullContainer() != null, OrderEntity::getContainerMode,
query.fullContainer() == null ? null : query.fullContainer() ? "FULL_CONTAINER" : "LCL")
.apply(StringUtils.hasText(query.fbaCode()),
"EXISTS (SELECT 1 FROM ord_order_cargo c WHERE c.tenant_id = ord_order.tenant_id "
+ "AND c.order_id = ord_order.id AND c.deleted = 0 AND c.sku_no LIKE CONCAT('%', {0}, '%'))",
query.fbaCode())
.ge(query.createdFrom() != null, OrderEntity::getCreatedAt, query.createdFrom())
.le(query.createdTo() != null, OrderEntity::getCreatedAt, query.createdTo())
.and(StringUtils.hasText(query.keyword()), nested -> nested
@@ -175,6 +184,15 @@ public class OrderRepository {
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
}
public List<Long> findOrderIdsBySkuNo(Long tenantId, String fbaCode) {
if (!StringUtils.hasText(fbaCode)) return List.of();
QueryWrapper<OrderCargoEntity> wrapper = new QueryWrapper<OrderCargoEntity>()
.select("DISTINCT order_id")
.eq(tenantId != null, "tenant_id", tenantId)
.like("sku_no", fbaCode);
return cargoMapper.selectObjs(wrapper).stream().map(value -> ((Number) value).longValue()).toList();
}
public List<OrderCargoEntity> findCargoByOrderIds(long tenantId, List<Long> orderIds) {
if (orderIds.isEmpty()) return List.of();
return cargoMapper.selectList(new LambdaQueryWrapper<OrderCargoEntity>()
@@ -41,7 +41,10 @@ public class UnifiedOrderQueryService {
public PageResult<UnifiedOrderResponse.Row> page(Long tenantId, UnifiedOrderQuery query) {
validate(query);
if (StringUtils.hasText(query.mainStatus()) && !"PENDING_REVIEW".equals(query.mainStatus())) {
if (requiresFirstlegQuery(query)) {
if ("PENDING_REVIEW".equals(query.mainStatus())) {
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
}
return pageFromFirstleg(tenantId, query);
}
List<OrderStatus> statuses = "PENDING_REVIEW".equals(query.mainStatus())
@@ -77,9 +80,17 @@ public class UnifiedOrderQueryService {
}
private PageResult<UnifiedOrderResponse.Row> pageFromFirstleg(Long tenantId, UnifiedOrderQuery query) {
List<Long> candidateOrderIds = StringUtils.hasText(query.fbaCode())
? orders.findOrderIdsBySkuNo(tenantId, query.fbaCode()) : null;
if (candidateOrderIds != null && candidateOrderIds.isEmpty()) {
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
}
FirstlegUnifiedOrderClient.Query request = new FirstlegUnifiedOrderClient.Query(
tenantId, query.keyword(), query.orderNo(), query.mainStatus(), query.customerId(),
query.receivingWarehouseId(), query.destinationWarehouseId(), query.createdFrom(), query.createdTo(),
query.receivingWarehouseId(), query.destinationWarehouseId(), query.inboundCode(),
query.internalOrderStatus(), query.expectedArrivalDate(), query.feeReviewStatus(), query.channelName(),
query.containerNo(), query.cabinetNo(), query.sailingDate(), query.loaded(), query.fullContainer(),
query.measurementReviewStatus(), candidateOrderIds, query.createdFrom(), query.createdTo(),
query.page(), query.size());
PageResult<FirstlegUnifiedOrderClient.Summary> source = require(
call(() -> firstleg.page(internalToken, request)), "统一订单履约查询响应无效");
@@ -184,6 +195,20 @@ public class UnifiedOrderQueryService {
&& query.createdFrom().isAfter(query.createdTo())) {
throw new BizException(ResultCode.BAD_REQUEST, "创建时间起点不能晚于终点");
}
if (StringUtils.hasText(query.internalOrderStatus())) {
try { OrderStatus.valueOf(query.internalOrderStatus()); }
catch (IllegalArgumentException exception) {
throw new BizException(ResultCode.BAD_REQUEST, "订单内部状态不合法");
}
}
}
private boolean requiresFirstlegQuery(UnifiedOrderQuery query) {
return StringUtils.hasText(query.mainStatus()) && !"PENDING_REVIEW".equals(query.mainStatus())
|| StringUtils.hasText(query.inboundCode()) || query.expectedArrivalDate() != null
|| StringUtils.hasText(query.feeReviewStatus()) || StringUtils.hasText(query.containerNo())
|| StringUtils.hasText(query.cabinetNo()) || query.sailingDate() != null || query.loaded() != null
|| StringUtils.hasText(query.measurementReviewStatus());
}
private <T> ApiResponse<T> call(java.util.concurrent.Callable<ApiResponse<T>> invocation) {
@@ -23,6 +23,18 @@ paths:
- {name: customerId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: receivingWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: destinationWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- $ref: '#/components/parameters/UnifiedInboundCode'
- $ref: '#/components/parameters/UnifiedInternalOrderStatus'
- $ref: '#/components/parameters/UnifiedExpectedArrivalDate'
- $ref: '#/components/parameters/UnifiedFeeReviewStatus'
- $ref: '#/components/parameters/UnifiedChannelName'
- $ref: '#/components/parameters/UnifiedContainerNo'
- $ref: '#/components/parameters/UnifiedCabinetNo'
- $ref: '#/components/parameters/UnifiedSailingDate'
- $ref: '#/components/parameters/UnifiedLoaded'
- $ref: '#/components/parameters/UnifiedFullContainer'
- $ref: '#/components/parameters/UnifiedMeasurementReviewStatus'
- $ref: '#/components/parameters/UnifiedFbaCode'
- $ref: '#/components/parameters/UnifiedCreatedFrom'
- $ref: '#/components/parameters/UnifiedCreatedTo'
- $ref: '#/components/parameters/UnifiedPage'
@@ -54,6 +66,18 @@ paths:
- {name: customerId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: receivingWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: destinationWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- $ref: '#/components/parameters/UnifiedInboundCode'
- $ref: '#/components/parameters/UnifiedInternalOrderStatus'
- $ref: '#/components/parameters/UnifiedExpectedArrivalDate'
- $ref: '#/components/parameters/UnifiedFeeReviewStatus'
- $ref: '#/components/parameters/UnifiedChannelName'
- $ref: '#/components/parameters/UnifiedContainerNo'
- $ref: '#/components/parameters/UnifiedCabinetNo'
- $ref: '#/components/parameters/UnifiedSailingDate'
- $ref: '#/components/parameters/UnifiedLoaded'
- $ref: '#/components/parameters/UnifiedFullContainer'
- $ref: '#/components/parameters/UnifiedMeasurementReviewStatus'
- $ref: '#/components/parameters/UnifiedFbaCode'
- $ref: '#/components/parameters/UnifiedCreatedFrom'
- $ref: '#/components/parameters/UnifiedCreatedTo'
- $ref: '#/components/parameters/UnifiedPage'
@@ -200,6 +224,54 @@ components:
schema:
type: string
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
UnifiedInboundCode:
{name: inboundCode, in: query, required: false, schema: {type: string, maxLength: 64}}
UnifiedInternalOrderStatus:
name: internalOrderStatus
in: query
required: false
description: Order 内部生命周期状态;不是七态产品主状态
schema: {type: string, enum: [DRAFT, SUBMITTED, APPROVED, ACCEPTED, REJECTED, CANCELLED, PROCESSING, COMPLETED]}
UnifiedExpectedArrivalDate:
{name: expectedArrivalDate, in: query, required: false, schema: {type: string, format: date}}
UnifiedFeeReviewStatus:
name: feeReviewStatus
in: query
required: false
description: 当前最高版本入库费用审核状态;UNRECORDED 表示尚无费用记录
schema: {type: string, maxLength: 32}
UnifiedChannelName:
{name: channelName, in: query, required: false, schema: {type: string, maxLength: 128}}
UnifiedContainerNo:
{name: containerNo, in: query, required: false, schema: {type: string, maxLength: 64}}
UnifiedCabinetNo:
{name: cabinetNo, in: query, required: false, schema: {type: string, maxLength: 64}}
UnifiedSailingDate:
{name: sailingDate, in: query, required: false, schema: {type: string, format: date}}
UnifiedLoaded:
name: loaded
in: query
required: false
description: true 表示运输装载状态存在且不是 UNLOADED/PENDING;false 表示无该状态
schema: {type: boolean}
UnifiedFullContainer:
name: fullContainer
in: query
required: false
description: true 对应 FULL_CONTAINER,false 对应 LCL
schema: {type: boolean}
UnifiedMeasurementReviewStatus:
name: measurementReviewStatus
in: query
required: false
description: 测量审核状态;UNRECORDED 表示字段为空
schema: {type: string, maxLength: 32}
UnifiedFbaCode:
name: fbaCode
in: query
required: false
description: 兼容匹配订单货物 sku_no
schema: {type: string, maxLength: 64}
UnifiedCreatedFrom:
name: createdFrom
in: query
@@ -9,11 +9,13 @@ import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -68,8 +70,39 @@ class UnifiedOrderQueryServiceTest {
verify(orders, never()).unifiedPage(any(), any(), any());
}
@Test
void expandedFulfillmentFiltersAreForwardedBeforeDatabasePaging() {
UnifiedOrderQuery query = new UnifiedOrderQuery(
null, null, "LOADING", 30001L, 11L, 22L, "RK-77", "APPROVED",
LocalDate.of(2026, 8, 6), "APPROVED", "美森", "MSCU", "CAB-1",
LocalDate.of(2026, 8, 8), true, true, "UNRECORDED", "FBA-1",
null, null, 1, 20);
when(orders.findOrderIdsBySkuNo(10001L, "FBA-1")).thenReturn(List.of(77L));
when(firstleg.page(eq("internal-token"), any()))
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 1, 20, 0, 0)));
service.page(10001L, query);
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
verify(firstleg).page(eq("internal-token"), captor.capture());
assertThat(captor.getValue()).satisfies(request -> {
assertThat(request.inboundCode()).isEqualTo("RK-77");
assertThat(request.internalOrderStatus()).isEqualTo("APPROVED");
assertThat(request.expectedArrivalDate()).isEqualTo(LocalDate.of(2026, 8, 6));
assertThat(request.containerNo()).isEqualTo("MSCU");
assertThat(request.sailingDate()).isEqualTo(LocalDate.of(2026, 8, 8));
assertThat(request.loaded()).isTrue();
assertThat(request.fullContainer()).isTrue();
assertThat(request.measurementReviewStatus()).isEqualTo("UNRECORDED");
assertThat(request.candidateOrderIds()).containsExactly(77L);
});
}
private UnifiedOrderQuery query(String status) {
return new UnifiedOrderQuery(null, null, status, null, null, null, null, null, 1, 20);
return new UnifiedOrderQuery(null, null, status, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, 1, 20);
}
private OrderEntity order(OrderStatus status) {
@@ -2,7 +2,14 @@ package com.baiyue.warehouse.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.warehouse.entity.PalletEntity;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/** 托盘主数据 Mapper。 */
public interface PalletMapper extends BaseMapper<PalletEntity> {
/** 编码序列必须包含逻辑删除记录,避免历史编码被复用。 */
@Select("SELECT pallet_code FROM wh_pallet WHERE tenant_id = #{tenantId} AND pallet_code LIKE 'TP-%'")
List<String> selectAllCodes(@Param("tenantId") long tenantId);
}
@@ -27,7 +27,6 @@ public class PalletRepository {
public PageResult<PalletEntity> page(Long tenantId, PalletRequests.PageQuery query) {
LambdaQueryWrapper<PalletEntity> wrapper = new LambdaQueryWrapper<PalletEntity>()
.eq(tenantId != null, PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getDeleted, false)
.eq(query.warehouseId() != null, PalletEntity::getWarehouseId, query.warehouseId())
.like(StringUtils.hasText(query.palletCode()), PalletEntity::getPalletCode, query.palletCode())
.eq(StringUtils.hasText(query.palletType()), PalletEntity::getPalletType, query.palletType())
@@ -41,7 +40,6 @@ public class PalletRepository {
return mapper.selectOne(new LambdaQueryWrapper<PalletEntity>()
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.eq(PalletEntity::getDeleted, false)
.eq(PalletEntity::getId, id));
}
@@ -50,7 +48,6 @@ public class PalletRepository {
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.eq(PalletEntity::getPalletCode, code)
.eq(PalletEntity::getDeleted, false)
.ne(excludedId != null, PalletEntity::getId, excludedId)) > 0;
}
@@ -59,7 +56,6 @@ public class PalletRepository {
return mapper.selectList(new LambdaQueryWrapper<PalletEntity>()
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.eq(PalletEntity::getDeleted, false)
.eq(PalletEntity::getStatus, "AVAILABLE")
.eq(StringUtils.hasText(palletType), PalletEntity::getPalletType, palletType)
.eq(capacityCbm != null, PalletEntity::getCapacityCbm, capacityCbm)
@@ -67,15 +63,13 @@ public class PalletRepository {
.last("LIMIT " + limit));
}
public String nextCode(long tenantId, long warehouseId) {
int maxSequence = mapper.selectList(new LambdaQueryWrapper<PalletEntity>()
.select(PalletEntity::getPalletCode)
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.likeRight(PalletEntity::getPalletCode, "TP-")
.eq(PalletEntity::getDeleted, false))
/**
* 生成平台作用域内下一个编码。编码序列跨仓库共用,且包含逻辑删除记录,确保编码永不复用。
*/
public String nextCode(long tenantId) {
List<String> existing = mapper.selectAllCodes(tenantId);
int maxSequence = (existing == null ? List.<String>of() : existing)
.stream()
.map(PalletEntity::getPalletCode)
.map(PALLET_CODE::matcher)
.filter(Matcher::matches)
.mapToInt(matcher -> Integer.parseInt(matcher.group(1)))
@@ -16,6 +16,8 @@ import org.springframework.transaction.annotation.Transactional;
/** 托盘主数据应用服务。 */
@Service
public class PalletApplicationService {
private static final int CODE_INSERT_ATTEMPTS = 5;
@Resource
private PalletRepository repository;
@Resource
@@ -34,19 +36,23 @@ public class PalletApplicationService {
@Transactional
@BusinessAudit(event = "PALLET_CREATE", resourceType = "PALLET", description = "新增托盘", captureBefore = false)
public Long create(long tenantId, PalletRequests.SaveRequest request) {
requireWarehouse(tenantId, request.warehouseId());
requireWarehouse(tenantId, request.warehouseId(), true);
PalletEntity entity = new PalletEntity();
entity.setTenantId(tenantId);
entity.setWarehouseId(request.warehouseId());
entity.setPalletCode(repository.nextCode(tenantId, request.warehouseId()));
copy(entity, request);
entity.setStatus("AVAILABLE");
try {
repository.insert(entity);
} catch (DataIntegrityViolationException exception) {
throw new BizException(ResultCode.CONFLICT, "托盘编码已存在");
String code = repository.nextCode(tenantId);
for (int attempt = 0; attempt < CODE_INSERT_ATTEMPTS; attempt++) {
entity.setPalletCode(code);
try {
repository.insert(entity);
return entity.getId();
} catch (DataIntegrityViolationException exception) {
code = incrementCode(code);
}
}
return entity.getId();
throw new BizException(ResultCode.CONFLICT, "托盘编码生成冲突,请重试");
}
@Transactional
@@ -55,6 +61,7 @@ public class PalletApplicationService {
if (warehouseId != request.warehouseId()) {
throw new BizException(ResultCode.FORBIDDEN, "请求仓库与托盘所属仓库不一致");
}
requireWarehouse(tenantId, warehouseId, false);
PalletEntity entity = owned(tenantId, warehouseId, id);
copy(entity, request);
try {
@@ -83,10 +90,17 @@ public class PalletApplicationService {
return entity;
}
private void requireWarehouse(long tenantId, long warehouseId) {
if (warehouseRepository.findOwned(tenantId, warehouseId) == null) {
private void requireWarehouse(long tenantId, long warehouseId, boolean requireActive) {
var warehouse = warehouseRepository.findOwned(tenantId, warehouseId);
if (warehouse == null) {
throw new BizException(ResultCode.FORBIDDEN, "仓库不存在或不属于目标租户");
}
if (!"DOMESTIC".equalsIgnoreCase(warehouse.getWarehouseType())) {
throw new BizException(ResultCode.FORBIDDEN, "托盘仅支持国内仓");
}
if (requireActive && !"ACTIVE".equalsIgnoreCase(warehouse.getStatus())) {
throw new BizException(ResultCode.FORBIDDEN, "新增托盘仅支持启用中的国内仓");
}
}
private static void copy(PalletEntity entity, PalletRequests.SaveRequest request) {
@@ -97,6 +111,11 @@ public class PalletApplicationService {
entity.setRemark(request.remark());
}
private static String incrementCode(String code) {
int sequence = Integer.parseInt(code.substring("TP-".length())) + 1;
return "TP-%03d".formatted(sequence);
}
private static PalletRequests.View view(PalletEntity entity) {
return new PalletRequests.View(entity.getId(), entity.getTenantId(), entity.getWarehouseId(),
entity.getPalletCode(), entity.getPalletType(), entity.getCapacityCbm(), entity.getWeightKg(),
@@ -9,6 +9,7 @@ SET @sql = IF((SELECT COUNT(*) FROM information_schema.columns
'ALTER TABLE wh_pallet ADD COLUMN weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''托盘重量kg'' AFTER capacity_cbm',
'SELECT 1');
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
SET @sql = IF((SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet' AND column_name = 'color') = 0,
'ALTER TABLE wh_pallet ADD COLUMN color VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''托盘颜色'' AFTER weight_kg',
@@ -53,6 +54,35 @@ UPDATE wh_location SET tenant_id = 0 WHERE tenant_id <> 0;
UPDATE wh_location_reservation SET tenant_id = 0 WHERE tenant_id <> 0;
UPDATE wh_location_occupancy SET tenant_id = 0 WHERE tenant_id <> 0;
-- 托盘编码在平台公共作用域全局唯一,避免不同仓库并发生成重复编码。
DROP PROCEDURE IF EXISTS assert_pallet_code_globally_unique;
DELIMITER $$
CREATE PROCEDURE assert_pallet_code_globally_unique()
BEGIN
IF EXISTS (
SELECT 1 FROM wh_pallet
GROUP BY tenant_id, pallet_code
HAVING COUNT(*) > 1
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '托盘编码迁移失败:不同仓库存在重复编码,请先清理';
END IF;
END$$
DELIMITER ;
CALL assert_pallet_code_globally_unique();
DROP PROCEDURE IF EXISTS assert_pallet_code_globally_unique;
SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet'
AND index_name = 'uk_pallet_tenant_warehouse_code') > 0,
'ALTER TABLE wh_pallet DROP INDEX uk_pallet_tenant_warehouse_code', 'SELECT 1');
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet'
AND index_name = 'uk_pallet_tenant_code') = 0,
'ALTER TABLE wh_pallet ADD UNIQUE KEY uk_pallet_tenant_code (tenant_id, pallet_code)', 'SELECT 1');
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = @schema_name
AND table_name = 'wh_location_reservation'
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id),
UNIQUE KEY uk_pallet_tenant_warehouse_code (tenant_id, warehouse_id, pallet_code),
UNIQUE KEY uk_pallet_tenant_code (tenant_id, pallet_code),
KEY idx_pallet_available (tenant_id, warehouse_id, status, deleted),
KEY idx_pallet_type (tenant_id, warehouse_id, pallet_type, status, deleted),
CONSTRAINT fk_pallet_warehouse FOREIGN KEY (warehouse_id) REFERENCES wh_warehouse (id),
@@ -0,0 +1,17 @@
-- 二期托盘初始化数据。执行顺序:warehouse-seed.sql、phase2-pallet-location-schema.sql、增量迁移、本文件。
SET NAMES utf8mb4;
USE baiyue_warehouse;
-- 编码由平台维护接口自动生成;种子固定为 TP-001、TP-002。
INSERT INTO wh_pallet(id,tenant_id,warehouse_id,pallet_code,pallet_type,capacity_cbm,weight_kg,color,status,remark,created_by,updated_by)
VALUES
(190000000000000101,0,190000000000000001,'TP-001','木托盘',1.200000,8.000,'黄色','AVAILABLE',NULL,0,0),
(190000000000000102,0,190000000000000001,'TP-002','胶托盘',2.400000,12.000,'黑色','AVAILABLE',NULL,0,0)
ON DUPLICATE KEY UPDATE
tenant_id=0,
warehouse_id=VALUES(warehouse_id),
pallet_type=VALUES(pallet_type),
capacity_cbm=VALUES(capacity_cbm),
weight_kg=VALUES(weight_kg),
color=VALUES(color),
remark=VALUES(remark);
@@ -11,13 +11,6 @@ INSERT INTO wh_service_capability(id,tenant_id,warehouse_id,capability_code,capa
VALUES(190000000000000021,0,190000000000000001,'RECEIVING','头程收货',1,'16:00:00','工作日16点前预约可当日收货')
ON DUPLICATE KEY UPDATE tenant_id=0,enabled=1,cutoff_time=VALUES(cutoff_time);
-- 二期托盘初始化数据。编码由平台维护接口自动生成,种子固定为 TP-001、TP-002。
INSERT INTO wh_pallet(id,tenant_id,warehouse_id,pallet_code,pallet_type,capacity_cbm,weight_kg,color,status,remark,created_by,updated_by)
VALUES
(190000000000000101,0,190000000000000001,'TP-001','木托盘',1.200000,8.000,'黄色','AVAILABLE','无备注',0,0),
(190000000000000102,0,190000000000000001,'TP-002','胶托盘',2.400000,12.000,'黑色','AVAILABLE','无备注',0,0)
ON DUPLICATE KEY UPDATE tenant_id=0,warehouse_id=VALUES(warehouse_id),pallet_type=VALUES(pallet_type),capacity_cbm=VALUES(capacity_cbm),weight_kg=VALUES(weight_kg),color=VALUES(color),remark=VALUES(remark);
-- 一期联调快递公司主数据,供平台维护和订单履约回显验证。
INSERT INTO wh_express_company(id,tenant_id,company_code,company_name,service_phone,website,tracking_url_template,status,remark)
VALUES(192000000000000001,0,'SF','顺丰速运','95338','https://www.sf-express.com','https://www.sf-express.com/chn/sc/dynamic_function/waybill/#search/bill-number/{trackingNo}','ACTIVE','平台公共快递公司样本')
@@ -43,6 +43,9 @@ class WarehouseSchemaContractTest {
@Test
void shouldHandleCase04() throws IOException {
String sql = read("phase2-pallet-location-schema.sql");
assertColumns(sql, "wh_pallet", "tenant_id", "warehouse_id", "pallet_code", "pallet_type",
"capacity_cbm", "weight_kg", "color", "status", "remark", "deleted", "version");
assertThat(sql).contains("uk_pallet_tenant_code (tenant_id, pallet_code)");
assertColumns(sql, "wh_location", "tenant_id", "warehouse_id", "zone_code", "zone_name",
"location_code", "location_type", "max_weight_kg", "shared_overflow", "status", "remark", "deleted", "version");
assertColumns(sql, "wh_location_reservation", "idempotency_key", "business_id", "pallet_id",
@@ -4,6 +4,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.warehouse.dto.PalletRequests;
import com.baiyue.warehouse.entity.PalletEntity;
import com.baiyue.warehouse.entity.WarehouseEntity;
import com.baiyue.warehouse.repository.PalletRepository;
import com.baiyue.warehouse.repository.WarehouseRepository;
import org.junit.jupiter.api.Test;
@@ -16,6 +17,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
@SpringJUnitConfig(PalletApplicationService.class)
class PalletApplicationServiceTest {
@MockBean private PalletRepository repository;
@@ -46,6 +49,20 @@ class PalletApplicationServiceTest {
verify(repository).page(null, query);
}
@Test
void rejectsOverseasWarehouseForCreate() {
WarehouseEntity warehouse = new WarehouseEntity();
warehouse.setWarehouseType("OVERSEAS");
warehouse.setStatus("ACTIVE");
when(warehouseRepository.findOwned(0, 20)).thenReturn(warehouse);
PalletRequests.SaveRequest request = new PalletRequests.SaveRequest(
20L, "木托盘", new BigDecimal("1.2"), new BigDecimal("8"), "黄色", null);
assertThatThrownBy(() -> service.create(0, request))
.isInstanceOf(BizException.class)
.hasMessageContaining("仅支持国内仓");
}
private static PalletEntity pallet(String status) {
PalletEntity entity = new PalletEntity();
entity.setId(30L);