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