优化
This commit is contained in:
@@ -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()));
|
||||
|
||||
+3
-1
@@ -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','平台公共快递公司样本')
|
||||
|
||||
+3
@@ -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",
|
||||
|
||||
+17
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user