From 3f15f3ad2bf81707aaa6bc522f50c385c1887676 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 7 Aug 2026 19:40:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9A=82=E5=AD=98=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 65 ++++++++++++------- docs/系统状态枚举说明.md | 7 +- ...ormInboundMeasurementReviewController.java | 2 +- .../firstleg/domain/InboundProcessStatus.java | 4 +- .../dto/InboundMeasurementRequests.java | 1 + .../dto/InboundMeasurementResponses.java | 1 + .../firstleg/dto/MeasurementBatchCommand.java | 1 + .../firstleg/mapper/InboundOrderMapper.java | 2 +- .../mapper/LoadingPickItemMapper.java | 4 +- .../repository/FbaPricingRepository.java | 4 +- .../firstleg/service/FbaPricingService.java | 6 +- .../service/InboundMeasurementService.java | 34 +++++++++- .../service/MeasurementBatchService.java | 1 + .../service/MeasurementReviewService.java | 4 +- .../firstleg/service/SmartLoadingService.java | 2 +- .../service/UnifiedInboundQueryService.java | 14 ++-- .../split/mapper/SplitInboundMapper.java | 2 +- .../split/service/SplitPreloadService.java | 14 ++-- .../phase3-inbounded-status-unification.sql | 17 +++++ .../db/phase3-smart-loading-schema.sql | 22 +++++++ .../resources/openapi/fba-pricing-v1.yaml | 4 +- .../controller/FbaPricingContractTest.java | 2 +- ...alInboundOrderReferenceControllerTest.java | 2 +- ...boundStatusUnificationSqlContractTest.java | 35 ++++++++++ ...artLoadingCandidateMapperContractTest.java | 2 + .../service/FbaPricingServiceTest.java | 14 ++-- .../InboundMeasurementServiceTest.java | 36 +++++++++- .../service/MeasurementBatchServiceTest.java | 3 +- .../service/MeasurementReviewServiceTest.java | 2 +- .../UnifiedInboundQueryServiceTest.java | 18 ++--- .../service/SplitPreloadServiceTest.java | 6 +- .../baiyue/order/dto/UnifiedOrderQuery.java | 2 +- .../service/UnifiedOrderQueryService.java | 7 +- .../src/main/resources/openapi/order-v1.yaml | 2 +- .../service/UnifiedOrderQueryServiceTest.java | 8 +-- scripts/phase2-test-env-acceptance.ps1 | 6 +- 36 files changed, 260 insertions(+), 96 deletions(-) create mode 100644 firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql create mode 100644 firstleg/src/test/java/com/baiyue/firstleg/repository/InboundStatusUnificationSqlContractTest.java diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index ef5f93afa..97de08871 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -3837,6 +3837,28 @@ CREATE DATABASE IF NOT EXISTS baiyue_firstleg DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE baiyue_firstleg; +-- 兼容已执行二期初始化的存量库:PDA 拣货需要累计每个托盘分配已拣箱数。 +DROP PROCEDURE IF EXISTS phase3_add_allocation_picked_count; +DELIMITER $$ +CREATE PROCEDURE phase3_add_allocation_picked_count() +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_pallet_allocation' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_pallet_allocation' + AND column_name = 'picked_package_count' + ) THEN + ALTER TABLE fl_inbound_pallet_allocation + ADD COLUMN picked_package_count INT UNSIGNED NOT NULL DEFAULT 0 + COMMENT '已拣货箱数' AFTER allocated_package_count; + END IF; +END$$ +DELIMITER ; +CALL phase3_add_allocation_picked_count(); +DROP PROCEDURE phase3_add_allocation_picked_count; + CREATE TABLE IF NOT EXISTS fl_container_booking ( id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID(雪花ID)', booking_no VARCHAR(40) NOT NULL COMMENT '平台唯一货柜预订单号', @@ -5748,28 +5770,6 @@ BEGIN PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; - IF (SELECT COUNT(*) FROM information_schema.statistics - WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order' - AND index_name = 'uk_order_tenant_inbound_code') <> 2 THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = '统一迁移失败:订单入仓码唯一索引未正确创建'; - END IF; - IF EXISTS ( - SELECT 1 FROM baiyue_order.ord_order - WHERE deleted = 0 - AND ((status = 'DRAFT' AND inbound_code IS NOT NULL) - OR (status <> 'DRAFT' AND inbound_code IS NULL)) - ) THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = '统一迁移失败:订单状态与入仓码数据不一致'; - END IF; - IF (SELECT COUNT(*) FROM information_schema.tables - WHERE table_schema = 'baiyue_firstleg' - AND table_name IN ('fl_preload_plan', 'fl_split_operation', - 'fl_loading_evidence', 'fl_loading_operation', - 'fl_loading_pick_task')) <> 5 THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = '统一迁移失败:三期分柜、预装或装柜表未完整创建'; END IF; END$$ DELIMITER ; @@ -6328,3 +6328,24 @@ END$$ DELIMITER ; CALL phase3_assert_inbound_parent_child_schema(); DROP PROCEDURE IF EXISTS phase3_assert_inbound_parent_child_schema; + +-- ============================================================================ +-- 来源:firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql +-- ============================================================================ +-- 将已废弃的 WAIT_SPLIT 统一为 INBOUNDED。 +-- 执行前请确认所有服务已升级至本次状态契约;脚本可重复执行。 +USE baiyue_firstleg; + +UPDATE fl_inbound_order +SET process_status = 'INBOUNDED', + updated_at = CURRENT_TIMESTAMP(3), + version = version + 1 +WHERE process_status = 'WAIT_SPLIT'; + +UPDATE fl_inbound_status_history +SET from_status = 'INBOUNDED' +WHERE from_status = 'WAIT_SPLIT'; + +UPDATE fl_inbound_status_history +SET to_status = 'INBOUNDED' +WHERE to_status = 'WAIT_SPLIT'; diff --git a/docs/系统状态枚举说明.md b/docs/系统状态枚举说明.md index c0d76da8a..72fb192ab 100644 --- a/docs/系统状态枚举说明.md +++ b/docs/系统状态枚举说明.md @@ -54,7 +54,7 @@ | `PROCESSING` | 履约处理中。 | | `COMPLETED` | 订单完成。 | -统一订单列表的主状态是展示分组,不等于订单表的内部状态:`PENDING_REVIEW`、`WAIT_MEASURE`、`WAIT_SPLIT`、`INBOUNDED`、`LOADING`、`LOADED`、`DELIVERED`。 +统一订单列表的主状态是展示分组,不等于订单表的内部状态:`PENDING_REVIEW`、`WAIT_MEASURE`、`INBOUNDED`、`LOADING`、`LOADED`、`DELIVERED`。 ## 仓库与库位 @@ -89,14 +89,13 @@ | `WAIT_PRICING` | 测量通过或异常放行,等待计费。 | | `FEE_CONFIRMING` | 已生成费用,等待费用确认和发送。 | | `INBOUNDING` | 费用已确认,正在占用托盘、库位并完成入库。 | -| `INBOUNDED` | 入库完成,费用已发送给客户。 | -| `WAIT_SPLIT` | 已完成入库,等待智能装柜或预装确认。 | +| `INBOUNDED` | 入库完成,可进入智能装柜或预装确认;费用状态独立维护。 | | `LOADING` | 已进入装柜作业,PDA 可执行拣货。 | | `LOADED` | 装柜完成,等待派送完成。 | | `DELIVERED` | 已完成派送。 | | `VOIDED` | 已作废,不允许继续处理。 | -主链路为:`WAIT_MEASURE` -> `MEASURING` -> `MEASURE_PENDING` -> `WAIT_PRICING` -> `FEE_CONFIRMING` -> `INBOUNDING` -> `INBOUNDED` -> `WAIT_SPLIT` -> `LOADING` -> `LOADED` -> `DELIVERED`。测量驳回走 `MEASURE_PENDING` -> `MEASURE_REJECTED`;异常放行后回到 `WAIT_PRICING`。 +主链路为:`WAIT_MEASURE` -> `MEASURING` -> `MEASURE_PENDING` -> `WAIT_PRICING` -> `FEE_CONFIRMING` -> `INBOUNDING` -> `INBOUNDED` -> `LOADING` -> `LOADED` -> `DELIVERED`。测量驳回走 `MEASURE_PENDING` -> `MEASURE_REJECTED`;异常放行后回到 `WAIT_PRICING`。 ### 测量、重量和费用状态 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementReviewController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementReviewController.java index 8ddd133a3..733ba36ee 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementReviewController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementReviewController.java @@ -51,7 +51,7 @@ public class PlatformInboundMeasurementReviewController { @Valid @RequestBody MeasurementReviewRequest request) { validateScope(user, tenantId); service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request); - // 统一订单主线要求测量审核后直接进入 WAIT_SPLIT,不把计费、库位或其他资料 + // 统一订单主线要求测量审核后直接进入 INBOUNDED,不把计费、库位或其他资料 // 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。 return ApiResponse.success(null); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundProcessStatus.java b/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundProcessStatus.java index 20c39a063..1e5e621b0 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundProcessStatus.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundProcessStatus.java @@ -20,8 +20,6 @@ public final class InboundProcessStatus { public static final String INBOUNDING = "INBOUNDING"; /** 已入库完成,费用已发送给客户。 */ public static final String INBOUNDED = "INBOUNDED"; - /** 等待智能装柜或预装确认。 */ - public static final String WAIT_SPLIT = "WAIT_SPLIT"; /** 已进入装柜作业,PDA 可执行拣货。 */ public static final String LOADING = "LOADING"; /** 装柜完成,等待派送完成。 */ @@ -34,7 +32,7 @@ public final class InboundProcessStatus { public static final Set VALUES = Set.of( WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED, WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED, - WAIT_SPLIT, LOADING, LOADED, DELIVERED, VOIDED + LOADING, LOADED, DELIVERED, VOIDED ); private InboundProcessStatus() { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java index a5de64c5e..f56293e9a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java @@ -34,6 +34,7 @@ public final class InboundMeasurementRequests { } public record ItemRequest( + @NotNull @Positive Long inboundDetailId, @NotBlank @Size(max = 32) String itemNo, @NotBlank @Size(max = 64) String boxSpecification, @NotNull @Positive @Max(1000000) Integer packageCount, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java index fa496b7e6..a4698eb23 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java @@ -46,6 +46,7 @@ public final class InboundMeasurementResponses { public record Item( Long id, + Long inboundDetailId, String itemNo, String boxSpecification, Integer packageCount, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/MeasurementBatchCommand.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/MeasurementBatchCommand.java index c4b631e0f..3c3ed8dd1 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/MeasurementBatchCommand.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/MeasurementBatchCommand.java @@ -28,6 +28,7 @@ public record MeasurementBatchCommand( List items ) { public record Item( + Long inboundDetailId, String itemNo, String boxSpecification, int packageCount, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOrderMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOrderMapper.java index 9e727b3fe..6de2f68c4 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOrderMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOrderMapper.java @@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Update; public interface InboundOrderMapper extends BaseMapper { @Update(""" UPDATE fl_inbound_order - SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED', + SET process_status = 'INBOUNDED', measure_review_status = 'APPROVED', inbounded_at = COALESCE(inbounded_at, CURRENT_TIMESTAMP(3)), updated_by = #{operatorId}, version = version + 1 WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java index 3af7c51d9..bd92dbbed 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java @@ -37,9 +37,9 @@ public interface LoadingPickItemMapper extends BaseMapper @Update(""" UPDATE fl_loading_pick_item - SET picked_package_count = picked_package_count + #{count}, - item_status = CASE WHEN picked_package_count + #{count} = planned_package_count + SET item_status = CASE WHEN picked_package_count + #{count} = planned_package_count THEN 'PICKED' ELSE 'PICKING' END, + picked_package_count = picked_package_count + #{count}, version = version + 1 WHERE id = #{id} AND version = #{version} AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java index c110d52c0..33e3cb2a8 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java @@ -209,7 +209,7 @@ public class FbaPricingRepository { .eq(InboundPricingEntity::getProcessStatus, processStatus) .in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED")) .set(InboundPricingEntity::getFeeStatus, "CALCULATED"); - if (!"WAIT_SPLIT".equals(processStatus)) { + if (!"INBOUNDED".equals(processStatus)) { update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING"); } if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable); @@ -229,7 +229,7 @@ public class FbaPricingRepository { public int markInboundConfirmed(long tenantId, long inboundId) { return inbounds.update(null, new LambdaUpdateWrapper() .eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId) - .in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT")) + .in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "INBOUNDED")) .eq(InboundPricingEntity::getFeeStatus, "CALCULATED") .set(InboundPricingEntity::getFeeStatus, "CONFIRMED")); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java index 450a2d49a..c9fb77452 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java @@ -93,8 +93,8 @@ public class FbaPricingService { throw conflict("入库单状态已变化,费用试算未保存"); } complete(token, fee.getId(), now); - String nextStatus = "WAIT_SPLIT".equals(inbound.getProcessStatus()) - ? "WAIT_SPLIT" : "FEE_CONFIRMING"; + String nextStatus = "INBOUNDED".equals(inbound.getProcessStatus()) + ? "INBOUNDED" : "FEE_CONFIRMING"; return response(inbound, fee, rate.getUnitPrice(), nextStatus); } @@ -235,7 +235,7 @@ public class FbaPricingService { private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) { InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId); - if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus()) + if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDED").contains(inbound.getProcessStatus()) || !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) { throw conflict("当前入库单状态不允许费用试算"); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java index 45ce9cc73..2a09179d6 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -3,7 +3,9 @@ package com.baiyue.firstleg.service; import jakarta.annotation.Resource; import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.client.OrderCargoClient; import com.baiyue.firstleg.dto.InboundMeasurementRequests; import com.baiyue.firstleg.dto.InboundMeasurementResponses; import com.baiyue.firstleg.dto.MeasurementBatchCommand; @@ -17,6 +19,8 @@ import com.baiyue.firstleg.repository.InboundMeasurementRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import feign.FeignException; import java.math.BigDecimal; import java.math.RoundingMode; @@ -44,6 +48,10 @@ public class InboundMeasurementService { private InboundMeasurementRepository repository; @Resource private MeasurementBatchService batchService; + @Resource + private OrderCargoClient orderCargoClient; + @Value("${baiyue.security.internal-service-token:}") + private String internalToken; public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) { InboundOrderEntity inbound = requireInbound(tenantId, inboundId); @@ -60,13 +68,14 @@ public class InboundMeasurementService { long tenantId, long inboundId, long operatorId, InboundMeasurementRequests.SaveDraftRequest request) { InboundOrderEntity inbound = requireInbound(tenantId, inboundId); + validateInboundDetails(tenantId, inbound, request.items()); MeasurementBatchCommand command = new MeasurementBatchCommand( tenantId, inboundId, request.measurementId(), request.version(), MeasurementBatchService.SOURCE_MANUAL, "PLATFORM", null, null, null, LocalDateTime.now(), null, request.originalDocumentNo(), request.originalGrossWeightKg(), request.originalVolumeCbm(), request.actualGoodsName(), operatorId, request.items().stream().map(item -> new MeasurementBatchCommand.Item( - item.itemNo(), item.boxSpecification(), item.packageCount(), item.lengthCm(), + item.inboundDetailId(), item.itemNo(), item.boxSpecification(), item.packageCount(), item.lengthCm(), item.widthCm(), item.heightCm(), item.unitWeightKg(), item.intelligentImageFileId(), item.overLimitWarning(), item.outerLengthCm(), item.outerWidthCm(), item.outerHeightCm(), item.remark())).toList()); @@ -286,7 +295,7 @@ public class InboundMeasurementService { private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) { return new InboundMeasurementResponses.Item( - item.getId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(), + item.getId(), item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(), item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getGirthCm(), item.getUnitWeightKg(), item.getUnitVolumeCbm(), item.getTotalWeightKg(), item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(), @@ -311,6 +320,27 @@ public class InboundMeasurementService { return inbound; } + private void validateInboundDetails(long tenantId, InboundOrderEntity inbound, + List items) { + if (inbound.getOrderId() == null || inbound.getOrderId() <= 0) { + throw new BizException(ResultCode.CONFLICT, "入库单未关联有效订单,不能保存测量明细"); + } + for (InboundMeasurementRequests.ItemRequest item : items) { + try { + ApiResponse response = orderCargoClient.detail( + internalToken, tenantId, inbound.getOrderId(), item.inboundDetailId()); + if (response == null || !response.successful() || response.data() == null) { + String message = response == null || !StringUtils.hasText(response.message()) + ? "订单货物明细校验失败" : response.message(); + throw new BizException(ResultCode.of(response == null + ? ResultCode.SERVICE_UNAVAILABLE.code() : response.code()), message); + } + } catch (FeignException exception) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法校验入库明细"); + } + } + } + private InboundMeasurementEntity requireMeasurement(long tenantId, long inboundId, long measurementId) { InboundMeasurementEntity measurement = repository.findMeasurement(tenantId, inboundId, measurementId); if (measurement == null) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java index e7abff234..274c3760a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java @@ -140,6 +140,7 @@ public class MeasurementBatchService { BigDecimal totalVolume = unitVolume.multiply(packageCount).setScale(4, RoundingMode.CEILING); InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); + item.setInboundDetailId(source.inboundDetailId()); item.setItemNo(source.itemNo()); item.setBoxSpecification(source.boxSpecification()); item.setPackageCount(source.packageCount()); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java index 15b8b5a46..e8e3a9d7e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java @@ -20,7 +20,7 @@ import org.springframework.util.StringUtils; import java.time.LocalDateTime; /** - * 测量审核应用服务。客服确认测量数据后将入库单推进到产品主状态 WAIT_SPLIT; + * 测量审核应用服务。客服确认测量数据后将入库单推进到产品主状态 INBOUNDED; * 计费、库位和其他资料不作为本版本的强制门禁。 */ @Service @@ -80,7 +80,7 @@ public class MeasurementReviewService { repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), result, request.reason(), false, null, false, operatorId, operatorName)); repository.insertHistory(history(tenantId, inboundId, "MEASURE_PENDING", - approved ? "WAIT_SPLIT" : "MEASURE_REJECTED", + approved ? "INBOUNDED" : "MEASURE_REJECTED", approved ? "MEASURE_APPROVE" : "MEASURE_REJECT", request.reason(), operatorId, operatorName)); idempotency.complete(context, inboundId); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java index a643fb2b2..9a53a5342 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -52,7 +52,7 @@ public class SmartLoadingService { public Plan detail(long planId) { LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在"); - return draftView(plan, requireBooking(plan.getContainerBookingId())); + return view(plan, requireBooking(plan.getContainerBookingId())); } @Transactional 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 014a70ed6..b5cd37f46 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java @@ -24,11 +24,11 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -/** Read-only adapter from internal inbound states to the seven product main statuses. */ +/** Read-only adapter from internal inbound states to the product main statuses. */ @Service public class UnifiedInboundQueryService { private static final Set MAIN_STATUSES = Set.of( - "PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"); + "PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED"); private static final Set REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED"); @Resource private InboundOrderRepository inbounds; @@ -120,8 +120,8 @@ public class UnifiedInboundQueryService { if (status == null) return null; return switch (status) { case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE"; - case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT" -> "WAIT_SPLIT"; - case "INBOUNDED" -> "INBOUNDED"; + // WAIT_SPLIT is retained only to normalize records created before status unification. + case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED" -> "INBOUNDED"; case "LOADING", "LOADED", "DELIVERED" -> status; default -> null; }; @@ -132,7 +132,7 @@ public class UnifiedInboundQueryService { return switch (mainStatus) { case "PENDING_REVIEW" -> "APPROVE_ORDER"; case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null; - case "WAIT_SPLIT" -> !"WAIT_SPLIT".equals(processStatus) ? null + case "INBOUNDED" -> !List.of("INBOUNDED", "WAIT_SPLIT").contains(processStatus) ? null : "CONFIRMED".equals(preloadStatus) ? null : preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD"; case "LOADING" -> "COMPLETE_LOADING"; @@ -146,8 +146,8 @@ public class UnifiedInboundQueryService { if (!StringUtils.hasText(mainStatus)) return List.of(); return switch (mainStatus) { case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED"); - case "WAIT_SPLIT" -> List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT"); - case "INBOUNDED", "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus); + case "INBOUNDED" -> List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED"); + case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus); default -> List.of("__NO_MATCH__"); }; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/split/mapper/SplitInboundMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/split/mapper/SplitInboundMapper.java index c608e8c06..159ef5853 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/split/mapper/SplitInboundMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/split/mapper/SplitInboundMapper.java @@ -13,7 +13,7 @@ public interface SplitInboundMapper { @Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, " + "version=version+1 WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 " - + "AND process_status='WAIT_SPLIT' AND version=#{expectedVersion}") + + "AND process_status='INBOUNDED' AND version=#{expectedVersion}") int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId, @Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java b/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java index 4b4b66672..33112be3d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java @@ -42,7 +42,7 @@ public class SplitPreloadService { SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, SMART_SPLIT, key); if (replay != null) return replay; InboundSnapshot inbound = requireInbound(tenantId, inboundId); - if (!"WAIT_SPLIT".equals(inbound.processStatus())) { + if (!"INBOUNDED".equals(inbound.processStatus())) { throw new BizException(ResultCode.CONFLICT, "仅待分柜订单可以生成预装清单"); } PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId); @@ -67,7 +67,7 @@ public class SplitPreloadService { } PreloadPlanResponse result = response(repository.findPlan(tenantId, inboundId)); return complete(tenantId, inboundId, operatorId, SMART_SPLIT, key, - new SplitActionResponse("WAIT_SPLIT", result)); + new SplitActionResponse("INBOUNDED", result)); } @Transactional @@ -77,11 +77,11 @@ public class SplitPreloadService { SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, START_CONFIRMING, key); if (replay != null) return replay; InboundSnapshot inbound = requireInbound(tenantId, inboundId); - requireStatus(inbound, "WAIT_SPLIT", "仅待分柜订单可以提交预装清单确认"); + requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以提交预装清单确认"); PreloadPlanEntity plan = requirePlan(tenantId, inboundId); if (PreloadStatus.CONFIRMING.equals(plan.getStatus())) { return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key, - new SplitActionResponse("WAIT_SPLIT", response(plan))); + new SplitActionResponse("INBOUNDED", response(plan))); } if (!PreloadStatus.PENDING.equals(plan.getStatus()) || repository.changePlanStatus(tenantId, inboundId, PreloadStatus.PENDING, @@ -89,7 +89,7 @@ public class SplitPreloadService { throw concurrent(); } return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key, - new SplitActionResponse("WAIT_SPLIT", response(repository.findPlan(tenantId, inboundId)))); + new SplitActionResponse("INBOUNDED", response(repository.findPlan(tenantId, inboundId)))); } @Transactional @@ -103,7 +103,7 @@ public class SplitPreloadService { return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key, new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId)))); } - requireStatus(inbound, "WAIT_SPLIT", "仅待分柜订单可以确认预装清单"); + requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以确认预装清单"); PreloadPlanEntity plan = requirePlan(tenantId, inboundId); if (PreloadStatus.CONFIRMED.equals(plan.getStatus())) { throw new BizException(ResultCode.CONFLICT, "预装清单已确认但订单状态尚未推进,请刷新重试"); @@ -115,7 +115,7 @@ public class SplitPreloadService { if (repository.beginLoading(tenantId, inboundId, operatorId, inbound.version()) != 1) { throw concurrent(); } - addHistory(tenantId, inboundId, "WAIT_SPLIT", "LOADING", CONFIRM_PRELOAD, + addHistory(tenantId, inboundId, "INBOUNDED", "LOADING", CONFIRM_PRELOAD, operatorId, operatorName); return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key, new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId)))); diff --git a/firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql b/firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql new file mode 100644 index 000000000..dc3078e9a --- /dev/null +++ b/firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql @@ -0,0 +1,17 @@ +-- 将已废弃的 WAIT_SPLIT 统一为 INBOUNDED。 +-- 执行前请确认所有服务已升级至本次状态契约;脚本可重复执行。 +USE baiyue_firstleg; + +UPDATE fl_inbound_order +SET process_status = 'INBOUNDED', + updated_at = CURRENT_TIMESTAMP(3), + version = version + 1 +WHERE process_status = 'WAIT_SPLIT'; + +UPDATE fl_inbound_status_history +SET from_status = 'INBOUNDED' +WHERE from_status = 'WAIT_SPLIT'; + +UPDATE fl_inbound_status_history +SET to_status = 'INBOUNDED' +WHERE to_status = 'WAIT_SPLIT'; diff --git a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql index d6c805260..235f68c4d 100644 --- a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql +++ b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql @@ -4,6 +4,28 @@ CREATE DATABASE IF NOT EXISTS baiyue_firstleg DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE baiyue_firstleg; +-- 兼容已执行二期初始化的存量库:PDA 拣货需要累计每个托盘分配已拣箱数。 +DROP PROCEDURE IF EXISTS phase3_add_allocation_picked_count; +DELIMITER $$ +CREATE PROCEDURE phase3_add_allocation_picked_count() +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_pallet_allocation' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_pallet_allocation' + AND column_name = 'picked_package_count' + ) THEN + ALTER TABLE fl_inbound_pallet_allocation + ADD COLUMN picked_package_count INT UNSIGNED NOT NULL DEFAULT 0 + COMMENT '已拣货箱数' AFTER allocated_package_count; + END IF; +END$$ +DELIMITER ; +CALL phase3_add_allocation_picked_count(); +DROP PROCEDURE phase3_add_allocation_picked_count; + CREATE TABLE IF NOT EXISTS fl_container_booking ( id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID(雪花ID)', booking_no VARCHAR(40) NOT NULL COMMENT '平台唯一货柜预订单号', diff --git a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml index 796de5b83..3a816343a 100644 --- a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml +++ b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml @@ -54,14 +54,14 @@ paths: /api/platform/v1/inbounds/{inboundId}/fees/calculate: post: summary: 按审核通过测量快照试算 FBA 费用 - description: 精确匹配唯一客户规则和唯一船司价格;测量审核后即使主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。 + description: 精确匹配唯一客户规则和唯一船司价格;测量审核后即使主流程已到 INBOUNDED,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。 parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}] requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}} responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} /api/platform/v1/inbounds/{inboundId}/fees/confirm: post: summary: 确认最新费用快照 - description: 确认费用只更新费用状态;主流程已到 WAIT_SPLIT 时保持不变。 + description: 确认费用只更新费用状态;主流程已到 INBOUNDED 时保持不变。 parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}] responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} /api/platform/v1/inbounds/{inboundId}/fees/send: diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java index 1b2365bd7..22af59af5 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java @@ -48,7 +48,7 @@ class FbaPricingContractTest { .contains("/api/platform/v1/inbounds/{inboundId}/fees/send:") .contains("/api/platform/v1/inbounds/{inboundId}/fees/published:") .contains("Idempotency-Key") - .contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态") + .contains("主流程已到 INBOUNDED,也可独立试算且不回退主流程状态") .contains("该动作涉及后续仓储流程,本轮二期验收不调用") .contains("不回退 INBOUNDED 状态") .doesNotContain("应收") 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 2522afe1a..0931f2bad 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java @@ -54,7 +54,7 @@ class InternalInboundOrderReferenceControllerTest { @Test void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() { UnifiedInboundQuery request = new UnifiedInboundQuery( - 10001L, null, null, "WAIT_SPLIT", null, null, null, + 10001L, null, null, "INBOUNDED", 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)); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundStatusUnificationSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundStatusUnificationSqlContractTest.java new file mode 100644 index 000000000..569f601df --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundStatusUnificationSqlContractTest.java @@ -0,0 +1,35 @@ +package com.baiyue.firstleg.repository; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +class InboundStatusUnificationSqlContractTest { + @Test + void syncsStatusUnificationMigrationToUnifiedDeploymentScript() throws Exception { + String module = normalize(new ClassPathResource("db/phase3-inbounded-status-unification.sql") + .getContentAsString(StandardCharsets.UTF_8)).trim(); + String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8)); + + assertThat(unified) + .contains("-- 来源:firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql") + .contains(module); + } + + private static Path unifiedPath() { + Path path = Path.of("deploy/sql/头程数据库统一增量-MySQL8.sql"); + if (Files.exists(path)) { + return path; + } + return Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql").normalize(); + } + + private static String normalize(String value) { + return value.replace("\r\n", "\n").replace("\uFEFF", ""); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java index 58a49ad2c..2e8c0f92b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java @@ -21,6 +21,7 @@ class SmartLoadingCandidateMapperContractTest { .contains("m2.status='APPROVED'") .contains("i.inbound_detail_id IS NOT NULL") .contains("o.warehouse_id=#{warehouseId}") + .contains("o.process_status='INBOUNDED'") .contains("SELECT SUM(li.package_count)") .contains("li.deleted=0") .contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC"); @@ -37,6 +38,7 @@ class SmartLoadingCandidateMapperContractTest { .contains("m.status='APPROVED'") .contains("SELECT MAX(m2.id)") .contains("o.warehouse_id=#{warehouseId}") + .contains("o.process_status='INBOUNDED'") .contains("SELECT SUM(li.package_count)") .contains("FOR UPDATE"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java index 9ddda2d3c..952aa768f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java @@ -105,7 +105,7 @@ class FbaPricingServiceTest { void calculatesAtWaitSplitWithoutRollingBackProcessStatus() { readyRepository(); InboundPricingEntity inbound = inbound("100.000", "1.0000"); - inbound.setProcessStatus("WAIT_SPLIT"); + inbound.setProcessStatus("INBOUNDED"); CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING"); CarrierFbaRateEntity rate = rate("KG", "2.000000", "0.00"); when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); @@ -115,8 +115,8 @@ class FbaPricingServiceTest { FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "wait-split"); assertThat(response.feeStatus()).isEqualTo("CALCULATED"); - assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); - verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_SPLIT", "KG", + assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); + verify(repository).markInboundCalculated(TENANT, INBOUND, "INBOUNDED", "KG", new BigDecimal("167.000")); } @@ -124,7 +124,7 @@ class FbaPricingServiceTest { @DisplayName("待分柜边界确认费用后仍保持主流程状态") void confirmsAtWaitSplitWithoutRollingBackProcessStatus() { InboundPricingEntity inbound = inbound("10.000", "1.0000"); - inbound.setProcessStatus("WAIT_SPLIT"); + inbound.setProcessStatus("INBOUNDED"); inbound.setFeeStatus("CALCULATED"); InboundFeeEntity fee = savedFee("CALCULATED"); when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee); @@ -141,7 +141,7 @@ class FbaPricingServiceTest { FeeSnapshotResponse response = service.confirm( TENANT, OPERATOR, INBOUND, "confirm-wait-split"); - assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); + assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); verify(repository).markInboundConfirmed(TENANT, INBOUND); } @@ -183,7 +183,7 @@ class FbaPricingServiceTest { void returnsOriginalSnapshotForRepeatedCalculation() { InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L); InboundPricingEntity inbound = inbound("10", "1"); - inbound.setProcessStatus("WAIT_SPLIT"); + inbound.setProcessStatus("INBOUNDED"); when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed); when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED")); when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); @@ -193,7 +193,7 @@ class FbaPricingServiceTest { FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same"); assertThat(response.feeId()).isEqualTo(501L); - assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); + assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); verify(repository, never()).matchRules(any(), any()); verify(repository, never()).insertFee(any()); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index b234b81e7..068c70a34 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -1,7 +1,10 @@ package com.baiyue.firstleg.service; import com.baiyue.common.core.exception.BizException; +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.firstleg.client.OrderCargoClient; import com.baiyue.firstleg.dto.InboundMeasurementRequests; +import com.baiyue.firstleg.dto.MeasurementBatchCommand; import com.baiyue.firstleg.dto.InboundMeasurementResponses; import com.baiyue.firstleg.entity.InboundMeasurementEntity; import com.baiyue.firstleg.entity.InboundMeasurementItemEntity; @@ -24,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -34,6 +38,7 @@ import static org.mockito.Mockito.when; class InboundMeasurementServiceTest { @MockBean private InboundMeasurementRepository repository; @MockBean private MeasurementBatchService batchService; + @MockBean private OrderCargoClient orderCargoClient; @Resource private InboundMeasurementService service; @Test @@ -165,7 +170,7 @@ class InboundMeasurementServiceTest { @Test void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() { - InboundOrderEntity inbound = inbound("WAIT_SPLIT"); + InboundOrderEntity inbound = inbound("INBOUNDED"); inbound.setMeasureReviewStatus("APPROVED"); InboundMeasurementEntity measurement = new InboundMeasurementEntity(); measurement.setId(30L); @@ -183,13 +188,40 @@ class InboundMeasurementServiceTest { InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L, new InboundMeasurementRequests.SubmitRequest(30L, 2)); - assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT"); + assertThat(result.processStatus()).isEqualTo("INBOUNDED"); assertThat(result.measureReviewStatus()).isEqualTo("APPROVED"); assertThat(measurement.getStatus()).isEqualTo("APPROVED"); verify(repository, never()).updateMeasurement(any()); verify(repository).updateInbound(inbound); } + @Test + void savingDraftValidatesCargoAndPersistsInboundDetailBinding() { + InboundOrderEntity inbound = inbound("WAIT_MEASURE"); + inbound.setOrderId(80L); + InboundMeasurementEntity saved = new InboundMeasurementEntity(); + saved.setId(30L); + saved.setVersion(0); + saved.setStatus("DRAFT"); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(orderCargoClient.detail(eq(""), eq(1L), eq(80L), eq(80001L))).thenReturn( + ApiResponse.success(new OrderCargoClient.CargoBinding(80001L, 80L, 1, "Test cargo", + 2, new BigDecimal("6.500"), new BigDecimal("0.0480")))); + when(batchService.saveBatch(any())).thenReturn(saved); + + InboundMeasurementResponses.DraftSaved result = service.saveDraft(1L, 10L, 9L, + new InboundMeasurementRequests.SaveDraftRequest(null, 0, null, null, null, null, + List.of(new InboundMeasurementRequests.ItemRequest(80001L, "001", "A", 2, + new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), + new BigDecimal("3.250"), null, null, null, null, null, null)))); + + assertThat(result.measurementId()).isEqualTo(30L); + ArgumentCaptor command = ArgumentCaptor.forClass(MeasurementBatchCommand.class); + verify(batchService).saveBatch(command.capture()); + assertThat(command.getValue().items().get(0).inboundDetailId()).isEqualTo(80001L); + verify(orderCargoClient).detail("", 1L, 80L, 80001L); + } + private InboundOrderEntity inbound(String status) { InboundOrderEntity inbound = new InboundOrderEntity(); inbound.setId(10L); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementBatchServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementBatchServiceTest.java index 20e4d530f..6947c8dcc 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementBatchServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementBatchServiceTest.java @@ -79,6 +79,7 @@ class MeasurementBatchServiceTest { ArgumentCaptor> items = ArgumentCaptor.forClass(List.class); verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L), org.mockito.ArgumentMatchers.eq(100L), items.capture()); + assertThat(items.getValue().get(0).getInboundDetailId()).isEqualTo(80001L); assertThat(items.getValue().get(0).getTotalWeightKg()).isEqualByComparingTo("6.500"); } @@ -131,7 +132,7 @@ class MeasurementBatchServiceTest { private MeasurementBatchCommand.Item item(BigDecimal unitWeight) { return new MeasurementBatchCommand.Item( - "001", "A", 2, new BigDecimal("40"), new BigDecimal("30"), + 80001L, "001", "A", 2, new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), unitWeight, 500L, null, new BigDecimal("41"), new BigDecimal("31"), new BigDecimal("21"), "备注"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java index cd1a3c8c9..3907b012f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java @@ -76,7 +76,7 @@ class MeasurementReviewServiceTest { ArgumentCaptor.forClass(InboundStatusHistoryEntity.class); verify(fixture.repository).insertHistory(history.capture()); assertThat(history.getValue().getFromStatus()).isEqualTo("MEASURE_PENDING"); - assertThat(history.getValue().getToStatus()).isEqualTo("WAIT_SPLIT"); + assertThat(history.getValue().getToStatus()).isEqualTo("INBOUNDED"); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java index 7d3d2a564..45990e094 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java @@ -14,9 +14,10 @@ class UnifiedInboundQueryServiceTest { assertThat(UnifiedInboundQueryService.mainStatus("MEASURING")).isEqualTo("WAIT_MEASURE"); assertThat(UnifiedInboundQueryService.mainStatus("MEASURE_PENDING")).isEqualTo("WAIT_MEASURE"); assertThat(UnifiedInboundQueryService.mainStatus("MEASURE_REJECTED")).isEqualTo("WAIT_MEASURE"); - assertThat(UnifiedInboundQueryService.mainStatus("WAIT_PRICING")).isEqualTo("WAIT_SPLIT"); - assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("WAIT_SPLIT"); - assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("WAIT_SPLIT"); + assertThat(UnifiedInboundQueryService.mainStatus("WAIT_PRICING")).isEqualTo("INBOUNDED"); + assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("INBOUNDED"); + assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("INBOUNDED"); + assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("INBOUNDED"); assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED"); assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING"); assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED"); @@ -29,13 +30,14 @@ class UnifiedInboundQueryServiceTest { assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURING", null)).isNull(); assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURE_PENDING", null)) .isEqualTo("CONFIRM_MEASUREMENT"); - assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", null)) + assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", null)) .isEqualTo("SMART_SPLIT"); - assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "PENDING")) + assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "PENDING")) .isEqualTo("CONFIRM_PRELOAD"); - assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "INBOUNDING", null)).isNull(); - assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "CONFIRMED")).isNull(); - assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", null)).isNull(); + assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDING", null)).isNull(); + assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "CONFIRMED")).isNull(); + assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "WAIT_SPLIT", null)) + .isEqualTo("SMART_SPLIT"); assertThat(UnifiedInboundQueryService.nextAction("LOADED", "LOADED", null)) .isEqualTo("COMPLETE_DELIVERY"); assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull(); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/split/service/SplitPreloadServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/split/service/SplitPreloadServiceTest.java index 30b98bb0c..09dd84108 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/split/service/SplitPreloadServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/split/service/SplitPreloadServiceTest.java @@ -44,19 +44,19 @@ class SplitPreloadServiceTest { @Test void smartSplitCreatesEditablePendingPlan() { when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true); - when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 3)); + when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "INBOUNDED", 3)); when(repository.findPlan(10, 20)).thenReturn(null); SmartSplitRequest request = new SmartSplitRequest(0, List.of(new PreloadItem("C1", "A", 2, BigDecimal.ONE, null)), "r"); SplitActionResponse result = service.smartSplit(10, 20, 7, "admin", "k2", request); - assertThat(result.mainStatus()).isEqualTo("WAIT_SPLIT"); + assertThat(result.mainStatus()).isEqualTo("INBOUNDED"); verify(repository).createPlan(any(PreloadPlanEntity.class)); } @Test void confirmsPlanAndMovesInboundToLoading() { when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true); - when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 4)); + when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "INBOUNDED", 4)); PreloadPlanEntity plan = new PreloadPlanEntity(); plan.setTenantId(10L); plan.setInboundId(20L); plan.setStatus("CONFIRMING"); plan.setVersion(1); plan.setItemsJson("[]"); 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 fecdc4dd6..cc3d7d8ee 100644 --- a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java +++ b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java @@ -12,7 +12,7 @@ import java.time.LocalDateTime; public record UnifiedOrderQuery( @Size(max = 64) String keyword, @Size(max = 64) String orderNo, - @Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", + @Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED"}) @Size(max = 32) String mainStatus, Long customerId, 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 ac48ef282..b4016f935 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -30,7 +30,7 @@ import java.util.stream.Collectors; @Service public class UnifiedOrderQueryService { private static final Set MAIN_STATUSES = Set.of( - "PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"); + "PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED"); private static final Set REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED"); private static final List ACTIVE_ORDER_STATUSES = List.copyOf(EnumSet.of( OrderStatus.SUBMITTED, OrderStatus.APPROVED, OrderStatus.ACCEPTED, @@ -181,8 +181,9 @@ public class UnifiedOrderQueryService { if (status == null) return null; return switch (status) { case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE"; - case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT" -> "WAIT_SPLIT"; - case "INBOUNDED", "LOADING", "LOADED", "DELIVERED" -> status; + // WAIT_SPLIT is retained only to normalize records created before status unification. + case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED" -> "INBOUNDED"; + case "LOADING", "LOADED", "DELIVERED" -> status; default -> null; }; } diff --git a/order/src/main/resources/openapi/order-v1.yaml b/order/src/main/resources/openapi/order-v1.yaml index f3182588a..3bf897154 100644 --- a/order/src/main/resources/openapi/order-v1.yaml +++ b/order/src/main/resources/openapi/order-v1.yaml @@ -262,7 +262,7 @@ components: required: false schema: type: string - enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED] + enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED] UnifiedInboundCode: {name: inboundCode, in: query, required: false, description: 客户提交后生成的入仓码,待审核订单同样可查询, schema: {type: string, maxLength: 40}} UnifiedInternalOrderStatus: 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 59df0cb15..b41eb2d32 100644 --- a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java @@ -105,15 +105,15 @@ class UnifiedOrderQueryServiceTest { @Test void fulfillmentStatusFilterUsesFirstlegPagingWithoutPostPageFiltering() { - UnifiedOrderQuery query = query("WAIT_SPLIT"); - var summary = summary("WAIT_SPLIT", "CONFIRM_PRELOAD"); + UnifiedOrderQuery query = query("INBOUNDED"); + var summary = summary("INBOUNDED", "CONFIRM_PRELOAD"); when(firstleg.page(eq("internal-token"), any())) .thenReturn(ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1))); var result = service.page(null, query); assertThat(result.records()).singleElement().satisfies(row -> { - assertThat(row.mainStatus()).isEqualTo("WAIT_SPLIT"); + assertThat(row.mainStatus()).isEqualTo("INBOUNDED"); assertThat(row.nextAction()).isEqualTo("CONFIRM_PRELOAD"); assertThat(row.inboundId()).isEqualTo(88L); assertThat(row.inboundVersion()).isEqualTo(7); @@ -182,7 +182,7 @@ class UnifiedOrderQueryServiceTest { private FirstlegUnifiedOrderClient.Summary summary(String status, String action) { var inbound = new FirstlegUnifiedOrderClient.Inbound( - 88L, 10001L, 77L, "FL-77", "RK-77", "WAIT_SPLIT", "APPROVED", "APPROVED", 7, + 88L, 10001L, 77L, "FL-77", "RK-77", "INBOUNDED", "APPROVED", "APPROVED", 7, 30001L, "测试客户", 11L, "深圳仓", 22L, "ONT8", "美森", 10, null, null, null, null, null, null, null, null, null, null, null, null, LocalDateTime.of(2026, 8, 6, 10, 0), LocalDateTime.of(2026, 8, 6, 11, 0)); diff --git a/scripts/phase2-test-env-acceptance.ps1 b/scripts/phase2-test-env-acceptance.ps1 index 73f36eb68..647d3c1fb 100644 --- a/scripts/phase2-test-env-acceptance.ps1 +++ b/scripts/phase2-test-env-acceptance.ps1 @@ -408,16 +408,16 @@ try { $approvedInboundResponse = Invoke-Api GET "/api/platform/inbounds/$($ids.inboundId)?tenantId=$TenantId" $platformToken $approvedInbound = Require-Success '审核后回查入库单' $approvedInboundResponse $versions.inbound = [int]$approvedInbound.summary.version - Require-Condition '二期边界停在 WAIT_SPLIT' $approvedInboundResponse ($approvedInbound.summary.processStatus -eq 'WAIT_SPLIT' -and $approvedInbound.summary.measureReviewStatus -eq 'APPROVED') "processStatus=$($approvedInbound.summary.processStatus), reviewStatus=$($approvedInbound.summary.measureReviewStatus)" + Require-Condition '二期边界停在 INBOUNDED' $approvedInboundResponse ($approvedInbound.summary.processStatus -eq 'INBOUNDED' -and $approvedInbound.summary.measureReviewStatus -eq 'APPROVED') "processStatus=$($approvedInbound.summary.processStatus), reviewStatus=$($approvedInbound.summary.measureReviewStatus)" $feeResponse = Invoke-Api POST "/api/platform/v1/inbounds/$($ids.inboundId)/fees/calculate?tenantId=$TenantId" $platformToken @{} @{ 'Idempotency-Key' = (New-Key 'fee-calculate') } $calculatedFee = Require-Success '二期计费接口试算' $feeResponse $ids.feeId = [string]$calculatedFee.feeId - Require-Condition '计费试算保持 WAIT_SPLIT 主流程边界' $feeResponse ($calculatedFee.nextProcessStatus -eq 'WAIT_SPLIT') "nextProcessStatus=$($calculatedFee.nextProcessStatus)" + Require-Condition '计费试算保持 INBOUNDED 主流程边界' $feeResponse ($calculatedFee.nextProcessStatus -eq 'INBOUNDED') "nextProcessStatus=$($calculatedFee.nextProcessStatus)" $feeConfirmResponse = Invoke-Api POST "/api/platform/v1/inbounds/$($ids.inboundId)/fees/confirm?tenantId=$TenantId" $platformToken $null @{ 'Idempotency-Key' = (New-Key 'fee-confirm') } $confirmedFee = Require-Success '确认二期费用快照' $feeConfirmResponse - Require-Condition '费用确认后仍保持 WAIT_SPLIT 主流程边界' $feeConfirmResponse ($confirmedFee.feeStatus -eq 'CONFIRMED' -and $confirmedFee.nextProcessStatus -eq 'WAIT_SPLIT') "feeStatus=$($confirmedFee.feeStatus), nextProcessStatus=$($confirmedFee.nextProcessStatus)" + Require-Condition '费用确认后仍保持 INBOUNDED 主流程边界' $feeConfirmResponse ($confirmedFee.feeStatus -eq 'CONFIRMED' -and $confirmedFee.nextProcessStatus -eq 'INBOUNDED') "feeStatus=$($confirmedFee.feeStatus), nextProcessStatus=$($confirmedFee.nextProcessStatus)" } catch { $fatal = $_.Exception.Message