暂存提交
This commit is contained in:
@@ -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
@@ -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`。
|
||||||
|
|
||||||
### 测量、重量和费用状态
|
### 测量、重量和费用状态
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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("应收")
|
||||||
|
|||||||
+1
-1
@@ -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));
|
||||||
|
|||||||
+35
@@ -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", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-2
@@ -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"), "备注");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+10
-8
@@ -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();
|
||||||
|
|||||||
+3
-3
@@ -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));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user