暂存提交

This commit is contained in:
2026-08-07 19:40:42 +08:00
parent 7c17b1a76a
commit 3f15f3ad2b
36 changed files with 260 additions and 96 deletions
@@ -3837,6 +3837,28 @@ CREATE DATABASE IF NOT EXISTS baiyue_firstleg
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE baiyue_firstleg; 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 ( CREATE TABLE IF NOT EXISTS fl_container_booking (
id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID(雪花ID)', id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID(雪花ID)',
booking_no VARCHAR(40) NOT NULL COMMENT '平台唯一货柜预订单号', booking_no VARCHAR(40) NOT NULL COMMENT '平台唯一货柜预订单号',
@@ -5748,28 +5770,6 @@ BEGIN
PREPARE stmt FROM @ddl; PREPARE stmt FROM @ddl;
EXECUTE stmt; EXECUTE stmt;
DEALLOCATE PREPARE 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 IF;
END$$ END$$
DELIMITER ; DELIMITER ;
@@ -6328,3 +6328,24 @@ END$$
DELIMITER ; DELIMITER ;
CALL phase3_assert_inbound_parent_child_schema(); CALL phase3_assert_inbound_parent_child_schema();
DROP PROCEDURE IF EXISTS 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';
+3 -4
View File
@@ -54,7 +54,7 @@
| `PROCESSING` | 履约处理中。 | | `PROCESSING` | 履约处理中。 |
| `COMPLETED` | 订单完成。 | | `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` | 测量通过或异常放行,等待计费。 | | `WAIT_PRICING` | 测量通过或异常放行,等待计费。 |
| `FEE_CONFIRMING` | 已生成费用,等待费用确认和发送。 | | `FEE_CONFIRMING` | 已生成费用,等待费用确认和发送。 |
| `INBOUNDING` | 费用已确认,正在占用托盘、库位并完成入库。 | | `INBOUNDING` | 费用已确认,正在占用托盘、库位并完成入库。 |
| `INBOUNDED` | 入库完成,费用已发送给客户。 | | `INBOUNDED` | 入库完成,可进入智能装柜或预装确认;费用状态独立维护。 |
| `WAIT_SPLIT` | 已完成入库,等待智能装柜或预装确认。 |
| `LOADING` | 已进入装柜作业,PDA 可执行拣货。 | | `LOADING` | 已进入装柜作业,PDA 可执行拣货。 |
| `LOADED` | 装柜完成,等待派送完成。 | | `LOADED` | 装柜完成,等待派送完成。 |
| `DELIVERED` | 已完成派送。 | | `DELIVERED` | 已完成派送。 |
| `VOIDED` | 已作废,不允许继续处理。 | | `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`。
### 测量、重量和费用状态 ### 测量、重量和费用状态
@@ -51,7 +51,7 @@ public class PlatformInboundMeasurementReviewController {
@Valid @RequestBody MeasurementReviewRequest request) { @Valid @RequestBody MeasurementReviewRequest request) {
validateScope(user, tenantId); validateScope(user, tenantId);
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request); service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
// 统一订单主线要求测量审核后直接进入 WAIT_SPLIT,不把计费、库位或其他资料 // 统一订单主线要求测量审核后直接进入 INBOUNDED,不把计费、库位或其他资料
// 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。 // 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。
return ApiResponse.success(null); return ApiResponse.success(null);
} }
@@ -20,8 +20,6 @@ public final class InboundProcessStatus {
public static final String INBOUNDING = "INBOUNDING"; public static final String INBOUNDING = "INBOUNDING";
/** 已入库完成,费用已发送给客户。 */ /** 已入库完成,费用已发送给客户。 */
public static final String INBOUNDED = "INBOUNDED"; public static final String INBOUNDED = "INBOUNDED";
/** 等待智能装柜或预装确认。 */
public static final String WAIT_SPLIT = "WAIT_SPLIT";
/** 已进入装柜作业,PDA 可执行拣货。 */ /** 已进入装柜作业,PDA 可执行拣货。 */
public static final String LOADING = "LOADING"; public static final String LOADING = "LOADING";
/** 装柜完成,等待派送完成。 */ /** 装柜完成,等待派送完成。 */
@@ -34,7 +32,7 @@ public final class InboundProcessStatus {
public static final Set<String> VALUES = Set.of( public static final Set<String> VALUES = Set.of(
WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED, WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED,
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED, WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED,
WAIT_SPLIT, LOADING, LOADED, DELIVERED, VOIDED LOADING, LOADED, DELIVERED, VOIDED
); );
private InboundProcessStatus() { private InboundProcessStatus() {
@@ -34,6 +34,7 @@ public final class InboundMeasurementRequests {
} }
public record ItemRequest( public record ItemRequest(
@NotNull @Positive Long inboundDetailId,
@NotBlank @Size(max = 32) String itemNo, @NotBlank @Size(max = 32) String itemNo,
@NotBlank @Size(max = 64) String boxSpecification, @NotBlank @Size(max = 64) String boxSpecification,
@NotNull @Positive @Max(1000000) Integer packageCount, @NotNull @Positive @Max(1000000) Integer packageCount,
@@ -46,6 +46,7 @@ public final class InboundMeasurementResponses {
public record Item( public record Item(
Long id, Long id,
Long inboundDetailId,
String itemNo, String itemNo,
String boxSpecification, String boxSpecification,
Integer packageCount, Integer packageCount,
@@ -28,6 +28,7 @@ public record MeasurementBatchCommand(
List<Item> items List<Item> items
) { ) {
public record Item( public record Item(
Long inboundDetailId,
String itemNo, String itemNo,
String boxSpecification, String boxSpecification,
int packageCount, int packageCount,
@@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Update;
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> { public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
@Update(""" @Update("""
UPDATE fl_inbound_order 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)), inbounded_at = COALESCE(inbounded_at, CURRENT_TIMESTAMP(3)),
updated_by = #{operatorId}, version = version + 1 updated_by = #{operatorId}, version = version + 1
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0 WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
@@ -37,9 +37,9 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
@Update(""" @Update("""
UPDATE fl_loading_pick_item UPDATE fl_loading_pick_item
SET picked_package_count = picked_package_count + #{count}, SET item_status = CASE WHEN picked_package_count + #{count} = planned_package_count
item_status = CASE WHEN picked_package_count + #{count} = planned_package_count
THEN 'PICKED' ELSE 'PICKING' END, THEN 'PICKED' ELSE 'PICKING' END,
picked_package_count = picked_package_count + #{count},
version = version + 1 version = version + 1
WHERE id = #{id} AND version = #{version} WHERE id = #{id} AND version = #{version}
AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item
@@ -209,7 +209,7 @@ public class FbaPricingRepository {
.eq(InboundPricingEntity::getProcessStatus, processStatus) .eq(InboundPricingEntity::getProcessStatus, processStatus)
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED")) .in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
.set(InboundPricingEntity::getFeeStatus, "CALCULATED"); .set(InboundPricingEntity::getFeeStatus, "CALCULATED");
if (!"WAIT_SPLIT".equals(processStatus)) { if (!"INBOUNDED".equals(processStatus)) {
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING"); update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
} }
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable); if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
@@ -229,7 +229,7 @@ public class FbaPricingRepository {
public int markInboundConfirmed(long tenantId, long inboundId) { public int markInboundConfirmed(long tenantId, long inboundId) {
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>() return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId) .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") .eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED")); .set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
} }
@@ -93,8 +93,8 @@ public class FbaPricingService {
throw conflict("入库单状态已变化,费用试算未保存"); throw conflict("入库单状态已变化,费用试算未保存");
} }
complete(token, fee.getId(), now); complete(token, fee.getId(), now);
String nextStatus = "WAIT_SPLIT".equals(inbound.getProcessStatus()) String nextStatus = "INBOUNDED".equals(inbound.getProcessStatus())
? "WAIT_SPLIT" : "FEE_CONFIRMING"; ? "INBOUNDED" : "FEE_CONFIRMING";
return response(inbound, fee, rate.getUnitPrice(), nextStatus); return response(inbound, fee, rate.getUnitPrice(), nextStatus);
} }
@@ -235,7 +235,7 @@ public class FbaPricingService {
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) { private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
InboundPricingEntity inbound = requireApprovedInbound(tenantId, 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())) { || !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
throw conflict("当前入库单状态不允许费用试算"); throw conflict("当前入库单状态不允许费用试算");
} }
@@ -3,7 +3,9 @@ package com.baiyue.firstleg.service;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.OrderCargoClient;
import com.baiyue.firstleg.dto.InboundMeasurementRequests; import com.baiyue.firstleg.dto.InboundMeasurementRequests;
import com.baiyue.firstleg.dto.InboundMeasurementResponses; import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import com.baiyue.firstleg.dto.MeasurementBatchCommand; import com.baiyue.firstleg.dto.MeasurementBatchCommand;
@@ -17,6 +19,8 @@ import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import feign.FeignException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@@ -44,6 +48,10 @@ public class InboundMeasurementService {
private InboundMeasurementRepository repository; private InboundMeasurementRepository repository;
@Resource @Resource
private MeasurementBatchService batchService; private MeasurementBatchService batchService;
@Resource
private OrderCargoClient orderCargoClient;
@Value("${baiyue.security.internal-service-token:}")
private String internalToken;
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) { public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) {
InboundOrderEntity inbound = requireInbound(tenantId, inboundId); InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
@@ -60,13 +68,14 @@ public class InboundMeasurementService {
long tenantId, long inboundId, long operatorId, long tenantId, long inboundId, long operatorId,
InboundMeasurementRequests.SaveDraftRequest request) { InboundMeasurementRequests.SaveDraftRequest request) {
InboundOrderEntity inbound = requireInbound(tenantId, inboundId); InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
validateInboundDetails(tenantId, inbound, request.items());
MeasurementBatchCommand command = new MeasurementBatchCommand( MeasurementBatchCommand command = new MeasurementBatchCommand(
tenantId, inboundId, request.measurementId(), request.version(), tenantId, inboundId, request.measurementId(), request.version(),
MeasurementBatchService.SOURCE_MANUAL, "PLATFORM", null, null, null, MeasurementBatchService.SOURCE_MANUAL, "PLATFORM", null, null, null,
LocalDateTime.now(), null, request.originalDocumentNo(), LocalDateTime.now(), null, request.originalDocumentNo(),
request.originalGrossWeightKg(), request.originalVolumeCbm(), request.actualGoodsName(), request.originalGrossWeightKg(), request.originalVolumeCbm(), request.actualGoodsName(),
operatorId, request.items().stream().map(item -> new MeasurementBatchCommand.Item( 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.widthCm(), item.heightCm(), item.unitWeightKg(), item.intelligentImageFileId(),
item.overLimitWarning(), item.outerLengthCm(), item.outerWidthCm(), item.outerHeightCm(), item.overLimitWarning(), item.outerLengthCm(), item.outerWidthCm(), item.outerHeightCm(),
item.remark())).toList()); item.remark())).toList());
@@ -286,7 +295,7 @@ public class InboundMeasurementService {
private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) { private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) {
return new InboundMeasurementResponses.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.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getGirthCm(),
item.getUnitWeightKg(), item.getUnitVolumeCbm(), item.getTotalWeightKg(), item.getUnitWeightKg(), item.getUnitVolumeCbm(), item.getTotalWeightKg(),
item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(), item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(),
@@ -311,6 +320,27 @@ public class InboundMeasurementService {
return inbound; return inbound;
} }
private void validateInboundDetails(long tenantId, InboundOrderEntity inbound,
List<InboundMeasurementRequests.ItemRequest> items) {
if (inbound.getOrderId() == null || inbound.getOrderId() <= 0) {
throw new BizException(ResultCode.CONFLICT, "入库单未关联有效订单,不能保存测量明细");
}
for (InboundMeasurementRequests.ItemRequest item : items) {
try {
ApiResponse<OrderCargoClient.CargoBinding> 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) { private InboundMeasurementEntity requireMeasurement(long tenantId, long inboundId, long measurementId) {
InboundMeasurementEntity measurement = repository.findMeasurement(tenantId, inboundId, measurementId); InboundMeasurementEntity measurement = repository.findMeasurement(tenantId, inboundId, measurementId);
if (measurement == null) { if (measurement == null) {
@@ -140,6 +140,7 @@ public class MeasurementBatchService {
BigDecimal totalVolume = unitVolume.multiply(packageCount).setScale(4, RoundingMode.CEILING); BigDecimal totalVolume = unitVolume.multiply(packageCount).setScale(4, RoundingMode.CEILING);
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setInboundDetailId(source.inboundDetailId());
item.setItemNo(source.itemNo()); item.setItemNo(source.itemNo());
item.setBoxSpecification(source.boxSpecification()); item.setBoxSpecification(source.boxSpecification());
item.setPackageCount(source.packageCount()); item.setPackageCount(source.packageCount());
@@ -20,7 +20,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* 测量审核应用服务。客服确认测量数据后将入库单推进到产品主状态 WAIT_SPLIT; * 测量审核应用服务。客服确认测量数据后将入库单推进到产品主状态 INBOUNDED;
* 计费、库位和其他资料不作为本版本的强制门禁。 * 计费、库位和其他资料不作为本版本的强制门禁。
*/ */
@Service @Service
@@ -80,7 +80,7 @@ public class MeasurementReviewService {
repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), result, repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), result,
request.reason(), false, null, false, operatorId, operatorName)); request.reason(), false, null, false, operatorId, operatorName));
repository.insertHistory(history(tenantId, inboundId, "MEASURE_PENDING", 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)); approved ? "MEASURE_APPROVE" : "MEASURE_REJECT", request.reason(), operatorId, operatorName));
idempotency.complete(context, inboundId); idempotency.complete(context, inboundId);
} }
@@ -52,7 +52,7 @@ public class SmartLoadingService {
public Plan detail(long planId) { public Plan detail(long planId) {
LoadingPlanEntity plan = repository.plan(planId); LoadingPlanEntity plan = repository.plan(planId);
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在"); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
return draftView(plan, requireBooking(plan.getContainerBookingId())); return view(plan, requireBooking(plan.getContainerBookingId()));
} }
@Transactional @Transactional
@@ -24,11 +24,11 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; 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 @Service
public class UnifiedInboundQueryService { public class UnifiedInboundQueryService {
private static final Set<String> MAIN_STATUSES = Set.of( private static final Set<String> 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<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED"); private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
@Resource private InboundOrderRepository inbounds; @Resource private InboundOrderRepository inbounds;
@@ -120,8 +120,8 @@ public class UnifiedInboundQueryService {
if (status == null) return null; if (status == null) return null;
return switch (status) { return switch (status) {
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE"; case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE";
case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT" -> "WAIT_SPLIT"; // WAIT_SPLIT is retained only to normalize records created before status unification.
case "INBOUNDED" -> "INBOUNDED"; case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED" -> "INBOUNDED";
case "LOADING", "LOADED", "DELIVERED" -> status; case "LOADING", "LOADED", "DELIVERED" -> status;
default -> null; default -> null;
}; };
@@ -132,7 +132,7 @@ public class UnifiedInboundQueryService {
return switch (mainStatus) { return switch (mainStatus) {
case "PENDING_REVIEW" -> "APPROVE_ORDER"; case "PENDING_REVIEW" -> "APPROVE_ORDER";
case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null; 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 : "CONFIRMED".equals(preloadStatus) ? null
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD"; : preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
case "LOADING" -> "COMPLETE_LOADING"; case "LOADING" -> "COMPLETE_LOADING";
@@ -146,8 +146,8 @@ public class UnifiedInboundQueryService {
if (!StringUtils.hasText(mainStatus)) return List.of(); if (!StringUtils.hasText(mainStatus)) return List.of();
return switch (mainStatus) { return switch (mainStatus) {
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED"); 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" -> List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED");
case "INBOUNDED", "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus); case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
default -> List.of("__NO_MATCH__"); default -> List.of("__NO_MATCH__");
}; };
} }
@@ -13,7 +13,7 @@ public interface SplitInboundMapper {
@Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, " @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 " + "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, int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion); @Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@@ -42,7 +42,7 @@ public class SplitPreloadService {
SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, SMART_SPLIT, key); SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, SMART_SPLIT, key);
if (replay != null) return replay; if (replay != null) return replay;
InboundSnapshot inbound = requireInbound(tenantId, inboundId); InboundSnapshot inbound = requireInbound(tenantId, inboundId);
if (!"WAIT_SPLIT".equals(inbound.processStatus())) { if (!"INBOUNDED".equals(inbound.processStatus())) {
throw new BizException(ResultCode.CONFLICT, "仅待分柜订单可以生成预装清单"); throw new BizException(ResultCode.CONFLICT, "仅待分柜订单可以生成预装清单");
} }
PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId); PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId);
@@ -67,7 +67,7 @@ public class SplitPreloadService {
} }
PreloadPlanResponse result = response(repository.findPlan(tenantId, inboundId)); PreloadPlanResponse result = response(repository.findPlan(tenantId, inboundId));
return complete(tenantId, inboundId, operatorId, SMART_SPLIT, key, return complete(tenantId, inboundId, operatorId, SMART_SPLIT, key,
new SplitActionResponse("WAIT_SPLIT", result)); new SplitActionResponse("INBOUNDED", result));
} }
@Transactional @Transactional
@@ -77,11 +77,11 @@ public class SplitPreloadService {
SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, START_CONFIRMING, key); SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, START_CONFIRMING, key);
if (replay != null) return replay; if (replay != null) return replay;
InboundSnapshot inbound = requireInbound(tenantId, inboundId); InboundSnapshot inbound = requireInbound(tenantId, inboundId);
requireStatus(inbound, "WAIT_SPLIT", "仅待分柜订单可以提交预装清单确认"); requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以提交预装清单确认");
PreloadPlanEntity plan = requirePlan(tenantId, inboundId); PreloadPlanEntity plan = requirePlan(tenantId, inboundId);
if (PreloadStatus.CONFIRMING.equals(plan.getStatus())) { if (PreloadStatus.CONFIRMING.equals(plan.getStatus())) {
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key, 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()) if (!PreloadStatus.PENDING.equals(plan.getStatus())
|| repository.changePlanStatus(tenantId, inboundId, PreloadStatus.PENDING, || repository.changePlanStatus(tenantId, inboundId, PreloadStatus.PENDING,
@@ -89,7 +89,7 @@ public class SplitPreloadService {
throw concurrent(); throw concurrent();
} }
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key, 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 @Transactional
@@ -103,7 +103,7 @@ public class SplitPreloadService {
return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key, return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key,
new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId)))); new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId))));
} }
requireStatus(inbound, "WAIT_SPLIT", "仅待分柜订单可以确认预装清单"); requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以确认预装清单");
PreloadPlanEntity plan = requirePlan(tenantId, inboundId); PreloadPlanEntity plan = requirePlan(tenantId, inboundId);
if (PreloadStatus.CONFIRMED.equals(plan.getStatus())) { if (PreloadStatus.CONFIRMED.equals(plan.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "预装清单已确认但订单状态尚未推进,请刷新重试"); throw new BizException(ResultCode.CONFLICT, "预装清单已确认但订单状态尚未推进,请刷新重试");
@@ -115,7 +115,7 @@ public class SplitPreloadService {
if (repository.beginLoading(tenantId, inboundId, operatorId, inbound.version()) != 1) { if (repository.beginLoading(tenantId, inboundId, operatorId, inbound.version()) != 1) {
throw concurrent(); throw concurrent();
} }
addHistory(tenantId, inboundId, "WAIT_SPLIT", "LOADING", CONFIRM_PRELOAD, addHistory(tenantId, inboundId, "INBOUNDED", "LOADING", CONFIRM_PRELOAD,
operatorId, operatorName); operatorId, operatorName);
return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key, return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key,
new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId)))); new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId))));
@@ -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';
@@ -4,6 +4,28 @@ CREATE DATABASE IF NOT EXISTS baiyue_firstleg
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE baiyue_firstleg; 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 ( CREATE TABLE IF NOT EXISTS fl_container_booking (
id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID(雪花ID)', id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID(雪花ID)',
booking_no VARCHAR(40) NOT NULL COMMENT '平台唯一货柜预订单号', booking_no VARCHAR(40) NOT NULL COMMENT '平台唯一货柜预订单号',
@@ -54,14 +54,14 @@ paths:
/api/platform/v1/inbounds/{inboundId}/fees/calculate: /api/platform/v1/inbounds/{inboundId}/fees/calculate:
post: post:
summary: 按审核通过测量快照试算 FBA 费用 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'}] parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}} requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}}
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/confirm: /api/platform/v1/inbounds/{inboundId}/fees/confirm:
post: post:
summary: 确认最新费用快照 summary: 确认最新费用快照
description: 确认费用只更新费用状态;主流程已到 WAIT_SPLIT 时保持不变。 description: 确认费用只更新费用状态;主流程已到 INBOUNDED 时保持不变。
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}] parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/send: /api/platform/v1/inbounds/{inboundId}/fees/send:
@@ -48,7 +48,7 @@ class FbaPricingContractTest {
.contains("/api/platform/v1/inbounds/{inboundId}/fees/send:") .contains("/api/platform/v1/inbounds/{inboundId}/fees/send:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/published:") .contains("/api/platform/v1/inbounds/{inboundId}/fees/published:")
.contains("Idempotency-Key") .contains("Idempotency-Key")
.contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态") .contains("主流程已到 INBOUNDED,也可独立试算且不回退主流程状态")
.contains("该动作涉及后续仓储流程,本轮二期验收不调用") .contains("该动作涉及后续仓储流程,本轮二期验收不调用")
.contains("不回退 INBOUNDED 状态") .contains("不回退 INBOUNDED 状态")
.doesNotContain("应收") .doesNotContain("应收")
@@ -54,7 +54,7 @@ class InternalInboundOrderReferenceControllerTest {
@Test @Test
void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() { void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() {
UnifiedInboundQuery request = new UnifiedInboundQuery( 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, null, null, null, null, null, null, null, null, null, null,
null, null, 1, 20); null, null, 1, 20);
when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0)); when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
@@ -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", "");
}
}
@@ -21,6 +21,7 @@ class SmartLoadingCandidateMapperContractTest {
.contains("m2.status='APPROVED'") .contains("m2.status='APPROVED'")
.contains("i.inbound_detail_id IS NOT NULL") .contains("i.inbound_detail_id IS NOT NULL")
.contains("o.warehouse_id=#{warehouseId}") .contains("o.warehouse_id=#{warehouseId}")
.contains("o.process_status='INBOUNDED'")
.contains("SELECT SUM(li.package_count)") .contains("SELECT SUM(li.package_count)")
.contains("li.deleted=0") .contains("li.deleted=0")
.contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC"); .contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC");
@@ -37,6 +38,7 @@ class SmartLoadingCandidateMapperContractTest {
.contains("m.status='APPROVED'") .contains("m.status='APPROVED'")
.contains("SELECT MAX(m2.id)") .contains("SELECT MAX(m2.id)")
.contains("o.warehouse_id=#{warehouseId}") .contains("o.warehouse_id=#{warehouseId}")
.contains("o.process_status='INBOUNDED'")
.contains("SELECT SUM(li.package_count)") .contains("SELECT SUM(li.package_count)")
.contains("FOR UPDATE"); .contains("FOR UPDATE");
} }
@@ -105,7 +105,7 @@ class FbaPricingServiceTest {
void calculatesAtWaitSplitWithoutRollingBackProcessStatus() { void calculatesAtWaitSplitWithoutRollingBackProcessStatus() {
readyRepository(); readyRepository();
InboundPricingEntity inbound = inbound("100.000", "1.0000"); InboundPricingEntity inbound = inbound("100.000", "1.0000");
inbound.setProcessStatus("WAIT_SPLIT"); inbound.setProcessStatus("INBOUNDED");
CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING"); CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING");
CarrierFbaRateEntity rate = rate("KG", "2.000000", "0.00"); CarrierFbaRateEntity rate = rate("KG", "2.000000", "0.00");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
@@ -115,8 +115,8 @@ class FbaPricingServiceTest {
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "wait-split"); FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "wait-split");
assertThat(response.feeStatus()).isEqualTo("CALCULATED"); assertThat(response.feeStatus()).isEqualTo("CALCULATED");
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_SPLIT", "KG", verify(repository).markInboundCalculated(TENANT, INBOUND, "INBOUNDED", "KG",
new BigDecimal("167.000")); new BigDecimal("167.000"));
} }
@@ -124,7 +124,7 @@ class FbaPricingServiceTest {
@DisplayName("待分柜边界确认费用后仍保持主流程状态") @DisplayName("待分柜边界确认费用后仍保持主流程状态")
void confirmsAtWaitSplitWithoutRollingBackProcessStatus() { void confirmsAtWaitSplitWithoutRollingBackProcessStatus() {
InboundPricingEntity inbound = inbound("10.000", "1.0000"); InboundPricingEntity inbound = inbound("10.000", "1.0000");
inbound.setProcessStatus("WAIT_SPLIT"); inbound.setProcessStatus("INBOUNDED");
inbound.setFeeStatus("CALCULATED"); inbound.setFeeStatus("CALCULATED");
InboundFeeEntity fee = savedFee("CALCULATED"); InboundFeeEntity fee = savedFee("CALCULATED");
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee); when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
@@ -141,7 +141,7 @@ class FbaPricingServiceTest {
FeeSnapshotResponse response = service.confirm( FeeSnapshotResponse response = service.confirm(
TENANT, OPERATOR, INBOUND, "confirm-wait-split"); TENANT, OPERATOR, INBOUND, "confirm-wait-split");
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
verify(repository).markInboundConfirmed(TENANT, INBOUND); verify(repository).markInboundConfirmed(TENANT, INBOUND);
} }
@@ -183,7 +183,7 @@ class FbaPricingServiceTest {
void returnsOriginalSnapshotForRepeatedCalculation() { void returnsOriginalSnapshotForRepeatedCalculation() {
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L); InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
InboundPricingEntity inbound = inbound("10", "1"); InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("WAIT_SPLIT"); inbound.setProcessStatus("INBOUNDED");
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed); when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED")); when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
@@ -193,7 +193,7 @@ class FbaPricingServiceTest {
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same"); FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
assertThat(response.feeId()).isEqualTo(501L); 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()).matchRules(any(), any());
verify(repository, never()).insertFee(any()); verify(repository, never()).insertFee(any());
} }
@@ -1,7 +1,10 @@
package com.baiyue.firstleg.service; package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException; 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.InboundMeasurementRequests;
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
import com.baiyue.firstleg.dto.InboundMeasurementResponses; import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import com.baiyue.firstleg.entity.InboundMeasurementEntity; import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity; 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.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -34,6 +38,7 @@ import static org.mockito.Mockito.when;
class InboundMeasurementServiceTest { class InboundMeasurementServiceTest {
@MockBean private InboundMeasurementRepository repository; @MockBean private InboundMeasurementRepository repository;
@MockBean private MeasurementBatchService batchService; @MockBean private MeasurementBatchService batchService;
@MockBean private OrderCargoClient orderCargoClient;
@Resource private InboundMeasurementService service; @Resource private InboundMeasurementService service;
@Test @Test
@@ -165,7 +170,7 @@ class InboundMeasurementServiceTest {
@Test @Test
void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() { void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() {
InboundOrderEntity inbound = inbound("WAIT_SPLIT"); InboundOrderEntity inbound = inbound("INBOUNDED");
inbound.setMeasureReviewStatus("APPROVED"); inbound.setMeasureReviewStatus("APPROVED");
InboundMeasurementEntity measurement = new InboundMeasurementEntity(); InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(30L); measurement.setId(30L);
@@ -183,13 +188,40 @@ class InboundMeasurementServiceTest {
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L, InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
new InboundMeasurementRequests.SubmitRequest(30L, 2)); new InboundMeasurementRequests.SubmitRequest(30L, 2));
assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT"); assertThat(result.processStatus()).isEqualTo("INBOUNDED");
assertThat(result.measureReviewStatus()).isEqualTo("APPROVED"); assertThat(result.measureReviewStatus()).isEqualTo("APPROVED");
assertThat(measurement.getStatus()).isEqualTo("APPROVED"); assertThat(measurement.getStatus()).isEqualTo("APPROVED");
verify(repository, never()).updateMeasurement(any()); verify(repository, never()).updateMeasurement(any());
verify(repository).updateInbound(inbound); 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<MeasurementBatchCommand> 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) { private InboundOrderEntity inbound(String status) {
InboundOrderEntity inbound = new InboundOrderEntity(); InboundOrderEntity inbound = new InboundOrderEntity();
inbound.setId(10L); inbound.setId(10L);
@@ -79,6 +79,7 @@ class MeasurementBatchServiceTest {
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class); ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L), verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
org.mockito.ArgumentMatchers.eq(100L), items.capture()); org.mockito.ArgumentMatchers.eq(100L), items.capture());
assertThat(items.getValue().get(0).getInboundDetailId()).isEqualTo(80001L);
assertThat(items.getValue().get(0).getTotalWeightKg()).isEqualByComparingTo("6.500"); assertThat(items.getValue().get(0).getTotalWeightKg()).isEqualByComparingTo("6.500");
} }
@@ -131,7 +132,7 @@ class MeasurementBatchServiceTest {
private MeasurementBatchCommand.Item item(BigDecimal unitWeight) { private MeasurementBatchCommand.Item item(BigDecimal unitWeight) {
return new MeasurementBatchCommand.Item( 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("20"), unitWeight, 500L, null,
new BigDecimal("41"), new BigDecimal("31"), new BigDecimal("21"), "备注"); new BigDecimal("41"), new BigDecimal("31"), new BigDecimal("21"), "备注");
} }
@@ -76,7 +76,7 @@ class MeasurementReviewServiceTest {
ArgumentCaptor.forClass(InboundStatusHistoryEntity.class); ArgumentCaptor.forClass(InboundStatusHistoryEntity.class);
verify(fixture.repository).insertHistory(history.capture()); verify(fixture.repository).insertHistory(history.capture());
assertThat(history.getValue().getFromStatus()).isEqualTo("MEASURE_PENDING"); assertThat(history.getValue().getFromStatus()).isEqualTo("MEASURE_PENDING");
assertThat(history.getValue().getToStatus()).isEqualTo("WAIT_SPLIT"); assertThat(history.getValue().getToStatus()).isEqualTo("INBOUNDED");
} }
@Test @Test
@@ -14,9 +14,10 @@ class UnifiedInboundQueryServiceTest {
assertThat(UnifiedInboundQueryService.mainStatus("MEASURING")).isEqualTo("WAIT_MEASURE"); assertThat(UnifiedInboundQueryService.mainStatus("MEASURING")).isEqualTo("WAIT_MEASURE");
assertThat(UnifiedInboundQueryService.mainStatus("MEASURE_PENDING")).isEqualTo("WAIT_MEASURE"); assertThat(UnifiedInboundQueryService.mainStatus("MEASURE_PENDING")).isEqualTo("WAIT_MEASURE");
assertThat(UnifiedInboundQueryService.mainStatus("MEASURE_REJECTED")).isEqualTo("WAIT_MEASURE"); assertThat(UnifiedInboundQueryService.mainStatus("MEASURE_REJECTED")).isEqualTo("WAIT_MEASURE");
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_PRICING")).isEqualTo("WAIT_SPLIT"); assertThat(UnifiedInboundQueryService.mainStatus("WAIT_PRICING")).isEqualTo("INBOUNDED");
assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("WAIT_SPLIT"); assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("INBOUNDED");
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("WAIT_SPLIT"); assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("INBOUNDED");
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("INBOUNDED");
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED"); assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED");
assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING"); assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING");
assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED"); 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", "MEASURING", null)).isNull();
assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURE_PENDING", null)) assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURE_PENDING", null))
.isEqualTo("CONFIRM_MEASUREMENT"); .isEqualTo("CONFIRM_MEASUREMENT");
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", null)) assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", null))
.isEqualTo("SMART_SPLIT"); .isEqualTo("SMART_SPLIT");
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "PENDING")) assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "PENDING"))
.isEqualTo("CONFIRM_PRELOAD"); .isEqualTo("CONFIRM_PRELOAD");
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "INBOUNDING", null)).isNull(); assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDING", null)).isNull();
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "CONFIRMED")).isNull(); assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "CONFIRMED")).isNull();
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", null)).isNull(); assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "WAIT_SPLIT", null))
.isEqualTo("SMART_SPLIT");
assertThat(UnifiedInboundQueryService.nextAction("LOADED", "LOADED", null)) assertThat(UnifiedInboundQueryService.nextAction("LOADED", "LOADED", null))
.isEqualTo("COMPLETE_DELIVERY"); .isEqualTo("COMPLETE_DELIVERY");
assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull(); assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull();
@@ -44,19 +44,19 @@ class SplitPreloadServiceTest {
@Test @Test
void smartSplitCreatesEditablePendingPlan() { void smartSplitCreatesEditablePendingPlan() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true); 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); when(repository.findPlan(10, 20)).thenReturn(null);
SmartSplitRequest request = new SmartSplitRequest(0, SmartSplitRequest request = new SmartSplitRequest(0,
List.of(new PreloadItem("C1", "A", 2, BigDecimal.ONE, null)), "r"); List.of(new PreloadItem("C1", "A", 2, BigDecimal.ONE, null)), "r");
SplitActionResponse result = service.smartSplit(10, 20, 7, "admin", "k2", request); 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)); verify(repository).createPlan(any(PreloadPlanEntity.class));
} }
@Test @Test
void confirmsPlanAndMovesInboundToLoading() { void confirmsPlanAndMovesInboundToLoading() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true); 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(); PreloadPlanEntity plan = new PreloadPlanEntity();
plan.setTenantId(10L); plan.setInboundId(20L); plan.setStatus("CONFIRMING"); plan.setVersion(1); plan.setTenantId(10L); plan.setInboundId(20L); plan.setStatus("CONFIRMING"); plan.setVersion(1);
plan.setItemsJson("[]"); plan.setItemsJson("[]");
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
public record UnifiedOrderQuery( public record UnifiedOrderQuery(
@Size(max = 64) String keyword, @Size(max = 64) String keyword,
@Size(max = 64) String orderNo, @Size(max = 64) String orderNo,
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", @Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED",
"LOADING", "LOADED", "DELIVERED"}) "LOADING", "LOADED", "DELIVERED"})
@Size(max = 32) String mainStatus, @Size(max = 32) String mainStatus,
Long customerId, Long customerId,
@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
@Service @Service
public class UnifiedOrderQueryService { public class UnifiedOrderQueryService {
private static final Set<String> MAIN_STATUSES = Set.of( private static final Set<String> 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<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED"); private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
private static final List<OrderStatus> ACTIVE_ORDER_STATUSES = List.copyOf(EnumSet.of( private static final List<OrderStatus> ACTIVE_ORDER_STATUSES = List.copyOf(EnumSet.of(
OrderStatus.SUBMITTED, OrderStatus.APPROVED, OrderStatus.ACCEPTED, OrderStatus.SUBMITTED, OrderStatus.APPROVED, OrderStatus.ACCEPTED,
@@ -181,8 +181,9 @@ public class UnifiedOrderQueryService {
if (status == null) return null; if (status == null) return null;
return switch (status) { return switch (status) {
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE"; case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE";
case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT" -> "WAIT_SPLIT"; // WAIT_SPLIT is retained only to normalize records created before status unification.
case "INBOUNDED", "LOADING", "LOADED", "DELIVERED" -> status; case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED" -> "INBOUNDED";
case "LOADING", "LOADED", "DELIVERED" -> status;
default -> null; default -> null;
}; };
} }
@@ -262,7 +262,7 @@ components:
required: false required: false
schema: schema:
type: string type: string
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED] enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED]
UnifiedInboundCode: UnifiedInboundCode:
{name: inboundCode, in: query, required: false, description: 客户提交后生成的入仓码,待审核订单同样可查询, schema: {type: string, maxLength: 40}} {name: inboundCode, in: query, required: false, description: 客户提交后生成的入仓码,待审核订单同样可查询, schema: {type: string, maxLength: 40}}
UnifiedInternalOrderStatus: UnifiedInternalOrderStatus:
@@ -105,15 +105,15 @@ class UnifiedOrderQueryServiceTest {
@Test @Test
void fulfillmentStatusFilterUsesFirstlegPagingWithoutPostPageFiltering() { void fulfillmentStatusFilterUsesFirstlegPagingWithoutPostPageFiltering() {
UnifiedOrderQuery query = query("WAIT_SPLIT"); UnifiedOrderQuery query = query("INBOUNDED");
var summary = summary("WAIT_SPLIT", "CONFIRM_PRELOAD"); var summary = summary("INBOUNDED", "CONFIRM_PRELOAD");
when(firstleg.page(eq("internal-token"), any())) when(firstleg.page(eq("internal-token"), any()))
.thenReturn(ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1))); .thenReturn(ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
var result = service.page(null, query); var result = service.page(null, query);
assertThat(result.records()).singleElement().satisfies(row -> { 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.nextAction()).isEqualTo("CONFIRM_PRELOAD");
assertThat(row.inboundId()).isEqualTo(88L); assertThat(row.inboundId()).isEqualTo(88L);
assertThat(row.inboundVersion()).isEqualTo(7); assertThat(row.inboundVersion()).isEqualTo(7);
@@ -182,7 +182,7 @@ class UnifiedOrderQueryServiceTest {
private FirstlegUnifiedOrderClient.Summary summary(String status, String action) { private FirstlegUnifiedOrderClient.Summary summary(String status, String action) {
var inbound = new FirstlegUnifiedOrderClient.Inbound( 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, 30001L, "测试客户", 11L, "深圳仓", 22L, "ONT8", "美森", 10, null, null, null,
null, null, null, null, null, null, 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)); LocalDateTime.of(2026, 8, 6, 10, 0), LocalDateTime.of(2026, 8, 6, 11, 0));
+3 -3
View File
@@ -408,16 +408,16 @@ try {
$approvedInboundResponse = Invoke-Api GET "/api/platform/inbounds/$($ids.inboundId)?tenantId=$TenantId" $platformToken $approvedInboundResponse = Invoke-Api GET "/api/platform/inbounds/$($ids.inboundId)?tenantId=$TenantId" $platformToken
$approvedInbound = Require-Success '审核后回查入库单' $approvedInboundResponse $approvedInbound = Require-Success '审核后回查入库单' $approvedInboundResponse
$versions.inbound = [int]$approvedInbound.summary.version $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') } $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 $calculatedFee = Require-Success '二期计费接口试算' $feeResponse
$ids.feeId = [string]$calculatedFee.feeId $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') } $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 $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 { catch {
$fatal = $_.Exception.Message $fatal = $_.Exception.Message