diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 784ba3cc1..6ff72d6a6 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -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; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java index ef8f7fcda..0b7b4c850 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java @@ -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 candidateOrderIds, LocalDateTime createdFrom, LocalDateTime createdTo, @Min(1) long page, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java index 4582af4a1..52c150b75 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java @@ -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); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java index fff815a8d..dba6db1c4 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java @@ -36,7 +36,8 @@ public class UnifiedInboundQueryService { public PageResult 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 source = inbounds.pageUnifiedEntities(query, internalStatuses(query.mainStatus())); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java index 020bb429e..a24689cdc 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java @@ -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(); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundOrderRepositoryTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundOrderRepositoryTest.java index 9d8eb8804..c7523ba68 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundOrderRepositoryTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundOrderRepositoryTest.java @@ -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(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> 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 wrapper = + (LambdaQueryWrapper) captor.getValue(); + assertThat(wrapper.getParamNameValuePairs().values()) + .contains(10001L, 30001L, 40001L, 50001L, "%RK-%", "APPROVED", "美森", + "MSCU", "CAB", 77L, 78L); + } + @Test @SuppressWarnings({"rawtypes", "unchecked"}) void shouldApplyTenantCompanyAndWarehouseFilters() { diff --git a/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java b/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java index 8a4846002..7dd7f1ec8 100644 --- a/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java +++ b/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java @@ -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 candidateOrderIds, LocalDateTime createdFrom, LocalDateTime createdTo, long page, long size) {} record BatchRequest(Long tenantId, List orderIds) {} record DetailRequest(long tenantId, long orderId) {} diff --git a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java index 1141db65a..fecdc4dd6 100644 --- a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java +++ b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java @@ -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, diff --git a/order/src/main/java/com/baiyue/order/repository/OrderRepository.java b/order/src/main/java/com/baiyue/order/repository/OrderRepository.java index 14a70363f..95a58f973 100644 --- a/order/src/main/java/com/baiyue/order/repository/OrderRepository.java +++ b/order/src/main/java/com/baiyue/order/repository/OrderRepository.java @@ -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 findOrderIdsBySkuNo(Long tenantId, String fbaCode) { + if (!StringUtils.hasText(fbaCode)) return List.of(); + QueryWrapper wrapper = new QueryWrapper() + .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 findCargoByOrderIds(long tenantId, List orderIds) { if (orderIds.isEmpty()) return List.of(); return cargoMapper.selectList(new LambdaQueryWrapper() diff --git a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java index 7f94d077e..d64df8f7c 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -41,7 +41,10 @@ public class UnifiedOrderQueryService { public PageResult 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 statuses = "PENDING_REVIEW".equals(query.mainStatus()) @@ -77,9 +80,17 @@ public class UnifiedOrderQueryService { } private PageResult pageFromFirstleg(Long tenantId, UnifiedOrderQuery query) { + List 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 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 ApiResponse call(java.util.concurrent.Callable> invocation) { diff --git a/order/src/main/resources/openapi/order-v1.yaml b/order/src/main/resources/openapi/order-v1.yaml index e550ba556..8042d7a4e 100644 --- a/order/src/main/resources/openapi/order-v1.yaml +++ b/order/src/main/resources/openapi/order-v1.yaml @@ -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 diff --git a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java index b42e01194..d57a14cc1 100644 --- a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java @@ -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 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) { diff --git a/warehouse/src/main/java/com/baiyue/warehouse/mapper/PalletMapper.java b/warehouse/src/main/java/com/baiyue/warehouse/mapper/PalletMapper.java index 693c8b691..9497d69b8 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/mapper/PalletMapper.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/mapper/PalletMapper.java @@ -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 { + /** 编码序列必须包含逻辑删除记录,避免历史编码被复用。 */ + @Select("SELECT pallet_code FROM wh_pallet WHERE tenant_id = #{tenantId} AND pallet_code LIKE 'TP-%'") + List selectAllCodes(@Param("tenantId") long tenantId); } diff --git a/warehouse/src/main/java/com/baiyue/warehouse/repository/PalletRepository.java b/warehouse/src/main/java/com/baiyue/warehouse/repository/PalletRepository.java index 8b8d3bd4d..d34bf08de 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/repository/PalletRepository.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/repository/PalletRepository.java @@ -27,7 +27,6 @@ public class PalletRepository { public PageResult page(Long tenantId, PalletRequests.PageQuery query) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .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() .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() .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() - .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 existing = mapper.selectAllCodes(tenantId); + int maxSequence = (existing == null ? List.of() : existing) .stream() - .map(PalletEntity::getPalletCode) .map(PALLET_CODE::matcher) .filter(Matcher::matches) .mapToInt(matcher -> Integer.parseInt(matcher.group(1))) diff --git a/warehouse/src/main/java/com/baiyue/warehouse/service/PalletApplicationService.java b/warehouse/src/main/java/com/baiyue/warehouse/service/PalletApplicationService.java index 23bb44810..7d5fff487 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/service/PalletApplicationService.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/service/PalletApplicationService.java @@ -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(), diff --git a/warehouse/src/main/resources/db/phase2-pallet-location-schema-migration.sql b/warehouse/src/main/resources/db/phase2-pallet-location-schema-migration.sql index d80b47b5f..14486e5a1 100644 --- a/warehouse/src/main/resources/db/phase2-pallet-location-schema-migration.sql +++ b/warehouse/src/main/resources/db/phase2-pallet-location-schema-migration.sql @@ -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' diff --git a/warehouse/src/main/resources/db/phase2-pallet-location-schema.sql b/warehouse/src/main/resources/db/phase2-pallet-location-schema.sql index 798c56dab..540b3a541 100644 --- a/warehouse/src/main/resources/db/phase2-pallet-location-schema.sql +++ b/warehouse/src/main/resources/db/phase2-pallet-location-schema.sql @@ -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), diff --git a/warehouse/src/main/resources/db/phase2-pallet-seed.sql b/warehouse/src/main/resources/db/phase2-pallet-seed.sql new file mode 100644 index 000000000..c97815071 --- /dev/null +++ b/warehouse/src/main/resources/db/phase2-pallet-seed.sql @@ -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); diff --git a/warehouse/src/main/resources/db/warehouse-seed.sql b/warehouse/src/main/resources/db/warehouse-seed.sql index 7772bf475..4011ae8da 100644 --- a/warehouse/src/main/resources/db/warehouse-seed.sql +++ b/warehouse/src/main/resources/db/warehouse-seed.sql @@ -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','平台公共快递公司样本') diff --git a/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseSchemaContractTest.java b/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseSchemaContractTest.java index 697f0fd70..3894f5d18 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseSchemaContractTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseSchemaContractTest.java @@ -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", diff --git a/warehouse/src/test/java/com/baiyue/warehouse/service/PalletApplicationServiceTest.java b/warehouse/src/test/java/com/baiyue/warehouse/service/PalletApplicationServiceTest.java index b566bc9ae..be0252b42 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/service/PalletApplicationServiceTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/service/PalletApplicationServiceTest.java @@ -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);