尾程暂扣回传
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
| 头程装柜完成 V2(当前生产) | `FirstLegLoadingPlanCompletedV2.java`;订单级冻结 `destinationType=FBA|OVERSEAS`,FBA 可按箱数接收,OVERSEAS 逐明细冻结与实装箱数一致的 SKU/箱号;同时冻结来源/根订单与入仓标识、客户、业务类型、目的仓/备注/托盘及一键代发托盘 SKU 快照;国内生产者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java`,海外消费者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java` |
|
| 头程装柜完成 V2(当前生产) | `FirstLegLoadingPlanCompletedV2.java`;订单级冻结 `destinationType=FBA|OVERSEAS`,FBA 可按箱数接收,OVERSEAS 逐明细冻结与实装箱数一致的 SKU/箱号;同时冻结来源/根订单与入仓标识、客户、业务类型、目的仓/备注/托盘及一键代发托盘 SKU 快照;国内生产者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java`,海外消费者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java` |
|
||||||
| 头程装柜完成 V1 解析模型 | `FirstLegLoadingPlanCompletedV1.java`;用于消费端冻结字段映射,新增生产不得回退为 V1 |
|
| 头程装柜完成 V1 解析模型 | `FirstLegLoadingPlanCompletedV1.java`;用于消费端冻结字段映射,新增生产不得回退为 V1 |
|
||||||
| 海外卸柜完成 V1 | `OverseasContainerUnloadedV1.java`;按来源柜预订 ID 做柜级聚合/幂等,同一柜只发布一次;`unloadingCompletedAt` 必须是全柜 complete 时刻,不得用签收拍照时刻代替;海外生产者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`,国内消费者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java` |
|
| 海外卸柜完成 V1 | `OverseasContainerUnloadedV1.java`;按来源柜预订 ID 做柜级聚合/幂等,同一柜只发布一次;`unloadingCompletedAt` 必须是全柜 complete 时刻,不得用签收拍照时刻代替;海外生产者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`,国内消费者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java` |
|
||||||
|
| 海外入仓单暂扣变更 V1 | `OverseasInboundOrderDetainedChangedV1.java`;聚合键固定为来源头程入仓单 ID,暂扣与解除暂扣均由尾程 Outbox 回传,国内按入仓单绑定、客户归属及聚合版本单调消费 |
|
||||||
| 国内船节点记录 V1 | `FirstLegContainerShippingNodeRecordedV1.java`;只接受十节点中带实际时间和完整供应商审计字段的事实,国内生产者 `../firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxService.java`,海外消费者 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
| 国内船节点记录 V1 | `FirstLegContainerShippingNodeRecordedV1.java`;只接受十节点中带实际时间和完整供应商审计字段的事实,国内生产者 `../firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxService.java`,海外消费者 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
||||||
| IAM 安全事件 V1 | `IamSecurityEventsV1.java`;账号、授权快照等安全事件的冻结模型 |
|
| IAM 安全事件 V1 | `IamSecurityEventsV1.java`;账号、授权快照等安全事件的冻结模型 |
|
||||||
| 契约说明 | `package-info.java` |
|
| 契约说明 | `package-info.java` |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public final class EventTypes {
|
|||||||
public static final String FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED =
|
public static final String FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED =
|
||||||
"FirstLegContainerShippingNodeRecorded";
|
"FirstLegContainerShippingNodeRecorded";
|
||||||
public static final String OVERSEAS_CONTAINER_UNLOADED = "OverseasContainerUnloaded";
|
public static final String OVERSEAS_CONTAINER_UNLOADED = "OverseasContainerUnloaded";
|
||||||
|
public static final String OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED = "OverseasInboundOrderDetainedChanged";
|
||||||
public static final String IAM_USER_STATUS_CHANGED = "IamUserStatusChanged";
|
public static final String IAM_USER_STATUS_CHANGED = "IamUserStatusChanged";
|
||||||
public static final String IAM_TENANT_STATUS_CHANGED = "IamTenantStatusChanged";
|
public static final String IAM_TENANT_STATUS_CHANGED = "IamTenantStatusChanged";
|
||||||
public static final String IAM_PERMISSION_VERSION_CHANGED = "IamPermissionVersionChanged";
|
public static final String IAM_PERMISSION_VERSION_CHANGED = "IamPermissionVersionChanged";
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.baiyue.common.event;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/** 海外尾程按来源入仓单回传的暂扣状态事实。 */
|
||||||
|
public record OverseasInboundOrderDetainedChangedV1(
|
||||||
|
String sourceInboundId,
|
||||||
|
String inboundCode,
|
||||||
|
String lastlegOrderId,
|
||||||
|
String lastlegOrderNo,
|
||||||
|
boolean detained,
|
||||||
|
String reason,
|
||||||
|
String operatorId,
|
||||||
|
String operatorName,
|
||||||
|
Instant changedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -2502,6 +2502,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
|||||||
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
||||||
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
||||||
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
||||||
|
last_overseas_hold_version BIGINT UNSIGNED NULL COMMENT '海外暂扣事件最新聚合版本,用于阻止乱序覆盖',
|
||||||
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
||||||
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '确认后的实际总毛重kg',
|
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '确认后的实际总毛重kg',
|
||||||
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
||||||
@@ -13504,6 +13505,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_at', '
|
|||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by', '创建人ID');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by', '创建人ID');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by_name', '创建人名称快照');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by_name', '创建人名称快照');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'current_hold_reason', '当前扣货理由快照');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'current_hold_reason', '当前扣货理由快照');
|
||||||
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'last_overseas_hold_version', '海外暂扣事件最新聚合版本,用于阻止乱序覆盖');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_id', '客户逻辑ID');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_id', '客户逻辑ID');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_name', '客户名称快照');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_name', '客户名称快照');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customs_inspection', '是否商检');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customs_inspection', '是否商检');
|
||||||
@@ -16298,12 +16300,35 @@ DROP PROCEDURE IF EXISTS add_container_empty_report_fields;
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_container_event_inbox (
|
CREATE TABLE IF NOT EXISTS fl_container_event_inbox (
|
||||||
event_id VARCHAR(64) NOT NULL COMMENT '跨区事件全局唯一ID', event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
event_id VARCHAR(64) NOT NULL COMMENT '跨区事件全局唯一ID', event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
||||||
event_version INT UNSIGNED NOT NULL COMMENT '事件契约版本', aggregate_id VARCHAR(64) NOT NULL COMMENT '货柜聚合ID',
|
event_version INT UNSIGNED NOT NULL COMMENT '事件契约版本', aggregate_id VARCHAR(64) NOT NULL COMMENT '跨区业务聚合ID',
|
||||||
aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '货柜聚合版本', payload_hash VARCHAR(80) NOT NULL COMMENT '规范化负载SHA-256摘要',
|
aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '跨区业务聚合版本', payload_hash VARCHAR(80) NOT NULL COMMENT '规范化负载SHA-256摘要',
|
||||||
payload_json JSON NOT NULL COMMENT '原始事件负载', status VARCHAR(16) NOT NULL COMMENT 'RECEIVED或PROCESSED',
|
payload_json JSON NOT NULL COMMENT '原始事件负载', status VARCHAR(16) NOT NULL COMMENT 'RECEIVED或PROCESSED',
|
||||||
received_at DATETIME(3) NOT NULL COMMENT '接收时间(UTC)', processed_at DATETIME(3) NULL COMMENT '处理完成时间(UTC)',
|
received_at DATETIME(3) NOT NULL COMMENT '接收时间(UTC)', processed_at DATETIME(3) NULL COMMENT '处理完成时间(UTC)',
|
||||||
PRIMARY KEY (event_id), KEY idx_container_event_aggregate (aggregate_id,aggregate_version)
|
PRIMARY KEY (event_id), KEY idx_container_event_aggregate (aggregate_id,aggregate_version)
|
||||||
) ENGINE=InnoDB COMMENT='海外货柜事件消费Inbox';
|
) ENGINE=InnoDB COMMENT='海外到国内业务事件消费Inbox';
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS add_inbound_overseas_hold_version;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE add_inbound_overseas_hold_version()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND table_name='fl_inbound_order'
|
||||||
|
AND column_name='last_overseas_hold_version'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE fl_inbound_order
|
||||||
|
ADD COLUMN last_overseas_hold_version BIGINT UNSIGNED NULL
|
||||||
|
COMMENT '海外暂扣事件最新聚合版本,用于阻止乱序覆盖'
|
||||||
|
AFTER current_hold_reason;
|
||||||
|
END IF;
|
||||||
|
ALTER TABLE fl_container_event_inbox
|
||||||
|
MODIFY COLUMN aggregate_id VARCHAR(64) NOT NULL COMMENT '跨区业务聚合ID',
|
||||||
|
MODIFY COLUMN aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '跨区业务聚合版本',
|
||||||
|
COMMENT='海外到国内业务事件消费Inbox';
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
CALL add_inbound_overseas_hold_version();
|
||||||
|
DROP PROCEDURE IF EXISTS add_inbound_overseas_hold_version;
|
||||||
|
|
||||||
-- 标准跨区事件摘要包含 sha256: 前缀,共 71 字符;兼容既有 Finance 64 位摘要。
|
-- 标准跨区事件摘要包含 sha256: 前缀,共 71 字符;兼容既有 Finance 64 位摘要。
|
||||||
ALTER TABLE fl_inbound_outbox
|
ALTER TABLE fl_inbound_outbox
|
||||||
|
|||||||
+1
@@ -30,6 +30,7 @@ public class EventBridgeMetrics {
|
|||||||
EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,
|
EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,
|
||||||
EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED,
|
EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED,
|
||||||
EventTypes.OVERSEAS_CONTAINER_UNLOADED,
|
EventTypes.OVERSEAS_CONTAINER_UNLOADED,
|
||||||
|
EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED,
|
||||||
EventTypes.IAM_USER_STATUS_CHANGED,
|
EventTypes.IAM_USER_STATUS_CHANGED,
|
||||||
EventTypes.IAM_TENANT_STATUS_CHANGED,
|
EventTypes.IAM_TENANT_STATUS_CHANGED,
|
||||||
EventTypes.IAM_PERMISSION_VERSION_CHANGED,
|
EventTypes.IAM_PERMISSION_VERSION_CHANGED,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
| PDA 拣货与装柜 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java` |
|
| PDA 拣货与装柜 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java` |
|
||||||
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType=FBA|OVERSEAS`,OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType=FBA|OVERSEAS`,OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||||
| 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
| 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||||
|
| 海外暂扣状态回写 | 同一内部事件入口、消费者 `service/OverseasInboundOrderDetainedConsumer.java`;按来源入仓单 ID 更新 `fl_inbound_order.hold_flag/current_hold_reason`,复用跨区 Inbox 并写 `fl_inbound_hold_record` |
|
||||||
| 三期海运订舱、关联候选与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`oceanbooking/mapper/OceanBookingMapper.java`、`portschedule/controller/PlatformPortScheduleController.java`;关联候选入口 `/api/platform/v1/ocean-bookings/link-candidates`,订舱契约见 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
|
| 三期海运订舱、关联候选与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`oceanbooking/mapper/OceanBookingMapper.java`、`portschedule/controller/PlatformPortScheduleController.java`;关联候选入口 `/api/platform/v1/ocean-bookings/link-candidates`,订舱契约见 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
|
||||||
| 四期国内出仓后船节点定时查询 | `outbound/dispatch/service/OutboundDispatchService.java` 在确认出仓事务内幂等创建现有 `CarrierScheduleQuery`;编排与租约 `portschedule/service/CarrierScheduleQueryScheduler.java`,任务/十节点事实/异常事实/不可变 attempt `CarrierScheduleQueryService.java`,平台查询与人工补录入口 `PlatformCarrierScheduleQueryController.java` 的 `/{queryId}/manual-nodes`;停止只由实际 `EMPTY_RETURNED` 节点触发,不提供无节点停止入口;外部船司/轨迹调用经 `integration-service/`;标准见 `../../docs/四期/船节点标准化契约.md` |
|
| 四期国内出仓后船节点定时查询 | `outbound/dispatch/service/OutboundDispatchService.java` 在确认出仓事务内幂等创建现有 `CarrierScheduleQuery`;编排与租约 `portschedule/service/CarrierScheduleQueryScheduler.java`,任务/十节点事实/异常事实/不可变 attempt `CarrierScheduleQueryService.java`,平台查询与人工补录入口 `PlatformCarrierScheduleQueryController.java` 的 `/{queryId}/manual-nodes`;停止只由实际 `EMPTY_RETURNED` 节点触发,不提供无节点停止入口;外部船司/轨迹调用经 `integration-service/`;标准见 `../../docs/四期/船节点标准化契约.md` |
|
||||||
| 船节点跨区交接事件 | `portschedule/service/CarrierScheduleNodeOutboxService.java`;实际节点落库后发布 `FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED` V1,复用 `LoadingCompletedOutboxRepository`/Relay 投 Event Bridge;海外消费见 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
| 船节点跨区交接事件 | `portschedule/service/CarrierScheduleNodeOutboxService.java`;实际节点落库后发布 `FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED` V1,复用 `LoadingCompletedOutboxRepository`/Relay 投 Event Bridge;海外消费见 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
||||||
|
|||||||
+7
-3
@@ -4,8 +4,9 @@ import com.baiyue.common.core.api.ApiResponse;
|
|||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.common.event.EventEnvelope;
|
import com.baiyue.common.event.EventEnvelope;
|
||||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
import com.baiyue.common.event.EventTypes;
|
||||||
import com.baiyue.firstleg.service.OverseasContainerEventConsumer;
|
import com.baiyue.firstleg.service.OverseasContainerEventConsumer;
|
||||||
|
import com.baiyue.firstleg.service.OverseasInboundOrderDetainedConsumer;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -20,14 +21,17 @@ import java.security.MessageDigest;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InternalOverseasContainerEventController {
|
public class InternalOverseasContainerEventController {
|
||||||
private final OverseasContainerEventConsumer consumer;
|
private final OverseasContainerEventConsumer consumer;
|
||||||
|
private final OverseasInboundOrderDetainedConsumer detainedConsumer;
|
||||||
@Value("${baiyue.security.internal-service-token}") private String expectedToken;
|
@Value("${baiyue.security.internal-service-token}") private String expectedToken;
|
||||||
|
|
||||||
@PostMapping("/internal/firstleg/v1/container-events")
|
@PostMapping("/internal/firstleg/v1/container-events")
|
||||||
public ApiResponse<ContainerBookingEntity> receive(
|
public ApiResponse<Object> receive(
|
||||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||||
@RequestBody EventEnvelope event) {
|
@RequestBody EventEnvelope event) {
|
||||||
authenticate(token);
|
authenticate(token);
|
||||||
return ApiResponse.success(consumer.consume(event));
|
Object result = EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED.equals(event.eventType())
|
||||||
|
? detainedConsumer.consume(event) : consumer.consume(event);
|
||||||
|
return ApiResponse.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void authenticate(String token) {
|
private void authenticate(String token) {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.baiyue.firstleg.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@TableName("fl_inbound_hold_record")
|
||||||
|
public class InboundHoldRecordEntity {
|
||||||
|
@TableId(type = IdType.INPUT) private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long inboundId;
|
||||||
|
private String holdAction;
|
||||||
|
private String reason;
|
||||||
|
private Long evidenceFileId;
|
||||||
|
private Long operatorId;
|
||||||
|
private String operatorName;
|
||||||
|
private LocalDateTime operatedAt;
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ public class InboundOrderEntity extends BaseEntity {
|
|||||||
private String destinationName;
|
private String destinationName;
|
||||||
private Boolean holdFlag;
|
private Boolean holdFlag;
|
||||||
private String currentHoldReason;
|
private String currentHoldReason;
|
||||||
|
private Long lastOverseasHoldVersion;
|
||||||
private Integer totalPackageCount;
|
private Integer totalPackageCount;
|
||||||
private BigDecimal actualGrossWeightKg;
|
private BigDecimal actualGrossWeightKg;
|
||||||
private BigDecimal chargeableWeightKg;
|
private BigDecimal chargeableWeightKg;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.baiyue.firstleg.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baiyue.firstleg.entity.InboundHoldRecordEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface InboundHoldRecordMapper extends BaseMapper<InboundHoldRecordEntity> {
|
||||||
|
}
|
||||||
@@ -5,10 +5,25 @@ import com.baiyue.firstleg.entity.InboundOrderEntity;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Update;
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
/** 入库聚合根条件更新 Mapper,所有推进均同时校验租户、前置状态和版本。 */
|
/** 入库聚合根条件更新 Mapper,所有推进均同时校验租户、前置状态和版本。 */
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||||
|
@Select("SELECT * FROM fl_inbound_order WHERE id=#{inboundId} AND deleted=0 FOR UPDATE")
|
||||||
|
InboundOrderEntity selectActiveByIdForUpdate(@Param("inboundId") long inboundId);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE fl_inbound_order
|
||||||
|
SET hold_flag=#{detained}, current_hold_reason=#{reason},
|
||||||
|
last_overseas_hold_version=#{sourceVersion}, updated_by=#{operatorId}, version=version+1
|
||||||
|
WHERE id=#{inboundId} AND tenant_id=#{tenantId} AND deleted=0
|
||||||
|
AND (last_overseas_hold_version IS NULL OR last_overseas_hold_version < #{sourceVersion})
|
||||||
|
""")
|
||||||
|
int applyOverseasHold(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||||
|
@Param("detained") boolean detained, @Param("reason") String reason,
|
||||||
|
@Param("sourceVersion") long sourceVersion, @Param("operatorId") long operatorId);
|
||||||
|
|
||||||
@Update("""
|
@Update("""
|
||||||
UPDATE fl_inbound_order
|
UPDATE fl_inbound_order
|
||||||
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
|
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
package com.baiyue.firstleg.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
|
import com.baiyue.common.event.EventEnvelope;
|
||||||
|
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||||
|
import com.baiyue.common.event.EventPayloadHash;
|
||||||
|
import com.baiyue.common.event.EventTypes;
|
||||||
|
import com.baiyue.common.event.OverseasInboundOrderDetainedChangedV1;
|
||||||
|
import com.baiyue.common.event.RegionCode;
|
||||||
|
import com.baiyue.firstleg.entity.ContainerEventInboxEntity;
|
||||||
|
import com.baiyue.firstleg.entity.InboundHoldRecordEntity;
|
||||||
|
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||||
|
import com.baiyue.firstleg.mapper.ContainerEventInboxMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundHoldRecordMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/** 将海外暂扣事实按来源入仓单绑定回写到国内头程入仓单。 */
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OverseasInboundOrderDetainedConsumer {
|
||||||
|
private final InboundOrderMapper inboundOrders;
|
||||||
|
private final InboundHoldRecordMapper holdRecords;
|
||||||
|
private final ContainerEventInboxMapper inbox;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public InboundOrderEntity consume(EventEnvelope event) {
|
||||||
|
validateEnvelope(event);
|
||||||
|
OverseasInboundOrderDetainedChangedV1 payload = payload(event);
|
||||||
|
long inboundId = parsePositive(payload.sourceInboundId(), "来源入仓单 ID 无效");
|
||||||
|
long tenantId = parsePositive(event.tenantId(), "客户 ID 无效");
|
||||||
|
if (!payload.sourceInboundId().equals(event.aggregateId())) {
|
||||||
|
throw new IllegalArgumentException("事件 aggregateId 与来源入仓单 ID 不一致");
|
||||||
|
}
|
||||||
|
if (payload.changedAt() == null || payload.inboundCode() == null || payload.inboundCode().isBlank()
|
||||||
|
|| payload.reason() == null || payload.reason().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("暂扣事件缺少入仓码、原因或变更时间");
|
||||||
|
}
|
||||||
|
|
||||||
|
ContainerEventInboxEntity existing = inbox.selectById(event.eventId());
|
||||||
|
if (existing != null) {
|
||||||
|
if (!existing.getPayloadHash().equalsIgnoreCase(event.payloadHash())) {
|
||||||
|
throw new IllegalArgumentException("相同 eventId 对应的事件摘要冲突");
|
||||||
|
}
|
||||||
|
return requiredInbound(inboundId, tenantId, payload.inboundCode());
|
||||||
|
}
|
||||||
|
ContainerEventInboxEntity inboxRow = inbox(event);
|
||||||
|
try {
|
||||||
|
inbox.insert(inboxRow);
|
||||||
|
} catch (DuplicateKeyException duplicate) {
|
||||||
|
ContainerEventInboxEntity raced = inbox.selectById(event.eventId());
|
||||||
|
if (raced == null || !raced.getPayloadHash().equalsIgnoreCase(event.payloadHash())) throw duplicate;
|
||||||
|
return requiredInbound(inboundId, tenantId, payload.inboundCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
InboundOrderEntity inbound = requiredInbound(inboundId, tenantId, payload.inboundCode());
|
||||||
|
if (inbound.getLastOverseasHoldVersion() == null
|
||||||
|
|| event.aggregateVersion() > inbound.getLastOverseasHoldVersion()) {
|
||||||
|
int updated = inboundOrders.applyOverseasHold(tenantId, inboundId, payload.detained(),
|
||||||
|
payload.detained() ? payload.reason() : null, event.aggregateVersion(),
|
||||||
|
parseOperatorId(payload.operatorId()));
|
||||||
|
if (updated == 1) insertHistory(event, payload, tenantId, inboundId);
|
||||||
|
}
|
||||||
|
inboxRow.setStatus("PROCESSED");
|
||||||
|
inboxRow.setProcessedAt(LocalDateTime.now(ZoneOffset.UTC));
|
||||||
|
inbox.updateById(inboxRow);
|
||||||
|
return requiredInbound(inboundId, tenantId, payload.inboundCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateEnvelope(EventEnvelope event) {
|
||||||
|
event.requireValid();
|
||||||
|
if (!EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED.equals(event.eventType())
|
||||||
|
|| event.eventVersion() != 1 || event.sourceRegion() != RegionCode.OVERSEAS
|
||||||
|
|| event.targetRegion() != RegionCode.CN || !"INBOUND_ORDER".equals(event.aggregateType())) {
|
||||||
|
throw new IllegalArgumentException("不支持的海外入仓单暂扣事件");
|
||||||
|
}
|
||||||
|
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(event.payload()), event.payloadHash())) {
|
||||||
|
throw new IllegalArgumentException("payloadHash 与事件负载不一致");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OverseasInboundOrderDetainedChangedV1 payload(EventEnvelope event) {
|
||||||
|
try { return objectMapper.treeToValue(event.payload(), OverseasInboundOrderDetainedChangedV1.class); }
|
||||||
|
catch (Exception exception) { throw new IllegalArgumentException("海外入仓单暂扣事件负载无效", exception); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private InboundOrderEntity requiredInbound(long inboundId, long tenantId, String inboundCode) {
|
||||||
|
InboundOrderEntity value = inboundOrders.selectActiveByIdForUpdate(inboundId);
|
||||||
|
if (value == null) throw new IllegalArgumentException("来源头程入仓单不存在");
|
||||||
|
if (!Objects.equals(value.getTenantId(), tenantId) || !Objects.equals(value.getInboundCode(), inboundCode)) {
|
||||||
|
throw new IllegalArgumentException("事件与来源头程入仓单归属不一致");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertHistory(EventEnvelope event, OverseasInboundOrderDetainedChangedV1 payload,
|
||||||
|
long tenantId, long inboundId) {
|
||||||
|
InboundHoldRecordEntity row = new InboundHoldRecordEntity();
|
||||||
|
row.setId(IdWorker.getId()); row.setTenantId(tenantId); row.setInboundId(inboundId);
|
||||||
|
row.setHoldAction(payload.detained() ? "HOLD" : "RELEASE"); row.setReason(payload.reason());
|
||||||
|
row.setOperatorId(parseOperatorId(payload.operatorId())); row.setOperatorName(payload.operatorName());
|
||||||
|
row.setOperatedAt(LocalDateTime.ofInstant(payload.changedAt(), ZoneOffset.UTC));
|
||||||
|
holdRecords.insert(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContainerEventInboxEntity inbox(EventEnvelope event) {
|
||||||
|
ContainerEventInboxEntity row = new ContainerEventInboxEntity();
|
||||||
|
row.setEventId(event.eventId()); row.setEventType(event.eventType()); row.setEventVersion(event.eventVersion());
|
||||||
|
row.setAggregateId(event.aggregateId()); row.setAggregateVersion(event.aggregateVersion());
|
||||||
|
row.setPayloadHash(event.payloadHash().toLowerCase(Locale.ROOT)); row.setPayloadJson(event.payload().toString());
|
||||||
|
row.setStatus("RECEIVED"); row.setReceivedAt(LocalDateTime.now(ZoneOffset.UTC));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parsePositive(String value, String message) {
|
||||||
|
try { long parsed = Long.parseLong(value); if (parsed > 0) return parsed; }
|
||||||
|
catch (RuntimeException ignored) { }
|
||||||
|
throw new IllegalArgumentException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseOperatorId(String value) {
|
||||||
|
try { return value == null ? 0L : Long.parseLong(value); }
|
||||||
|
catch (NumberFormatException ignored) { return 0L; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
|||||||
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
||||||
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
||||||
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
||||||
|
last_overseas_hold_version BIGINT UNSIGNED NULL COMMENT '海外暂扣事件最新聚合版本,用于阻止乱序覆盖',
|
||||||
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
||||||
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '已确认称重图片含托盘毛重汇总kg',
|
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '已确认称重图片含托盘毛重汇总kg',
|
||||||
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ CREATE TABLE IF NOT EXISTS fl_container_event_inbox (
|
|||||||
event_id VARCHAR(64) NOT NULL COMMENT '跨区事件全局唯一ID',
|
event_id VARCHAR(64) NOT NULL COMMENT '跨区事件全局唯一ID',
|
||||||
event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
||||||
event_version INT UNSIGNED NOT NULL COMMENT '事件契约版本',
|
event_version INT UNSIGNED NOT NULL COMMENT '事件契约版本',
|
||||||
aggregate_id VARCHAR(64) NOT NULL COMMENT '货柜聚合ID',
|
aggregate_id VARCHAR(64) NOT NULL COMMENT '跨区业务聚合ID',
|
||||||
aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '货柜聚合版本',
|
aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '跨区业务聚合版本',
|
||||||
payload_hash VARCHAR(80) NOT NULL COMMENT '规范化负载SHA-256摘要',
|
payload_hash VARCHAR(80) NOT NULL COMMENT '规范化负载SHA-256摘要',
|
||||||
payload_json JSON NOT NULL COMMENT '原始事件负载',
|
payload_json JSON NOT NULL COMMENT '原始事件负载',
|
||||||
status VARCHAR(16) NOT NULL COMMENT 'RECEIVED或PROCESSED',
|
status VARCHAR(16) NOT NULL COMMENT 'RECEIVED或PROCESSED',
|
||||||
@@ -111,7 +111,7 @@ CREATE TABLE IF NOT EXISTS fl_container_event_inbox (
|
|||||||
processed_at DATETIME(3) NULL COMMENT '处理完成时间(UTC)',
|
processed_at DATETIME(3) NULL COMMENT '处理完成时间(UTC)',
|
||||||
PRIMARY KEY (event_id),
|
PRIMARY KEY (event_id),
|
||||||
KEY idx_container_event_aggregate (aggregate_id, aggregate_version)
|
KEY idx_container_event_aggregate (aggregate_id, aggregate_version)
|
||||||
) ENGINE=InnoDB COMMENT='海外货柜事件消费Inbox';
|
) ENGINE=InnoDB COMMENT='海外到国内业务事件消费Inbox';
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS phase3_container_real_unit_columns;
|
DROP PROCEDURE IF EXISTS phase3_container_real_unit_columns;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
|
|||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package com.baiyue.firstleg.service;
|
||||||
|
|
||||||
|
import com.baiyue.common.event.EventEnvelope;
|
||||||
|
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||||
|
import com.baiyue.common.event.EventPayloadHash;
|
||||||
|
import com.baiyue.common.event.EventTypes;
|
||||||
|
import com.baiyue.common.event.OverseasInboundOrderDetainedChangedV1;
|
||||||
|
import com.baiyue.common.event.RegionCode;
|
||||||
|
import com.baiyue.firstleg.entity.InboundHoldRecordEntity;
|
||||||
|
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||||
|
import com.baiyue.firstleg.mapper.ContainerEventInboxMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundHoldRecordMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class OverseasInboundOrderDetainedConsumerTest {
|
||||||
|
@Mock InboundOrderMapper inboundOrders;
|
||||||
|
@Mock InboundHoldRecordMapper holdRecords;
|
||||||
|
@Mock ContainerEventInboxMapper inbox;
|
||||||
|
ObjectMapper objectMapper;
|
||||||
|
OverseasInboundOrderDetainedConsumer consumer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
|
consumer = new OverseasInboundOrderDetainedConsumer(inboundOrders, holdRecords, inbox, objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appliesHoldBySourceInboundIdAndWritesHistory() {
|
||||||
|
InboundOrderEntity inbound = inbound(null);
|
||||||
|
when(inboundOrders.selectActiveByIdForUpdate(1001L)).thenReturn(inbound);
|
||||||
|
when(inboundOrders.applyOverseasHold(88L, 1001L, true, "等待查验", 7L, 66L)).thenReturn(1);
|
||||||
|
|
||||||
|
consumer.consume(event(7, true, "等待查验"));
|
||||||
|
|
||||||
|
verify(inboundOrders).applyOverseasHold(88L, 1001L, true, "等待查验", 7L, 66L);
|
||||||
|
ArgumentCaptor<InboundHoldRecordEntity> history = ArgumentCaptor.forClass(InboundHoldRecordEntity.class);
|
||||||
|
verify(holdRecords).insert(history.capture());
|
||||||
|
assertThat(history.getValue().getInboundId()).isEqualTo(1001L);
|
||||||
|
assertThat(history.getValue().getHoldAction()).isEqualTo("HOLD");
|
||||||
|
verify(inbox).updateById(argThat(row -> "PROCESSED".equals(row.getStatus())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void staleReleaseCannotOverwriteNewerHoldState() {
|
||||||
|
when(inboundOrders.selectActiveByIdForUpdate(1001L)).thenReturn(inbound(9L));
|
||||||
|
|
||||||
|
consumer.consume(event(7, false, "解除"));
|
||||||
|
|
||||||
|
verify(inboundOrders, never()).applyOverseasHold(anyLong(), anyLong(), anyBoolean(), any(), anyLong(), anyLong());
|
||||||
|
verifyNoInteractions(holdRecords);
|
||||||
|
verify(inbox).updateById(argThat(row -> "PROCESSED".equals(row.getStatus())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private InboundOrderEntity inbound(Long sourceVersion) {
|
||||||
|
InboundOrderEntity value = new InboundOrderEntity();
|
||||||
|
value.setId(1001L); value.setTenantId(88L); value.setInboundCode("IN-1001");
|
||||||
|
value.setLastOverseasHoldVersion(sourceVersion);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventEnvelope event(long version, boolean detained, String reason) {
|
||||||
|
var payload = new OverseasInboundOrderDetainedChangedV1("1001", "IN-1001", "9001",
|
||||||
|
"LM-9001", detained, reason, "66", "海外操作员", Instant.parse("2026-08-31T06:00:00Z"));
|
||||||
|
var node = objectMapper.valueToTree(payload);
|
||||||
|
return new EventEnvelope(String.format("00000000-0000-4000-8000-%012d", version),
|
||||||
|
EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED, 1, payload.changedAt(),
|
||||||
|
RegionCode.OVERSEAS, RegionCode.CN, "88", "INBOUND_ORDER", "1001", version, 1L,
|
||||||
|
"00000000-0000-4000-8000-000000000099", "hold-1001-" + version, null, node,
|
||||||
|
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
| PDA 入仓、实收、上架与完成 | `inboundplan/pda/controller/ReceivingContainerController.java`、`inboundplan/pda/service/ReceivingContainerService.java`、内部编排 `InboundPdaWorkflowService.java`;公网入口固定为 `/api/pda/v1/last-leg/receiving-containers/**`,`/warehouse-profile` 只按 Token 绑定仓解析共享仓库中文名称与编码,旧 planNo/allocationId Controller 已移除 |
|
| PDA 入仓、实收、上架与完成 | `inboundplan/pda/controller/ReceivingContainerController.java`、`inboundplan/pda/service/ReceivingContainerService.java`、内部编排 `InboundPdaWorkflowService.java`;公网入口固定为 `/api/pda/v1/last-leg/receiving-containers/**`,`/warehouse-profile` 只按 Token 绑定仓解析共享仓库中文名称与编码,旧 planNo/allocationId Controller 已移除 |
|
||||||
| PDA 卸柜照片上传、归属校验与预览 | `ReceivingContainerController.java` 的 `/current/unload-photos/files`、`/current/unload-photos/files/{fileId}/download`,网关 `inboundplan/pda/gateway/UnloadPhotoFileGateway.java`、`HttpUnloadPhotoFileGateway.java`;File 类型固定 `OVERSEAS_UNLOAD_PHOTO`,下载只允许当前会话已签收的 fileId 并返回 `no-store` |
|
| PDA 卸柜照片上传、归属校验与预览 | `ReceivingContainerController.java` 的 `/current/unload-photos/files`、`/current/unload-photos/files/{fileId}/download`,网关 `inboundplan/pda/gateway/UnloadPhotoFileGateway.java`、`HttpUnloadPhotoFileGateway.java`;File 类型固定 `OVERSEAS_UNLOAD_PHOTO`,下载只允许当前会话已签收的 fileId 并返回 `no-store` |
|
||||||
| 卸柜照片与海外卸柜完成 Outbox | `ReceivingContainerController.java`、`ReceivingContainerService.java`、`inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`、`entity/LastlegEventOutbox.java`、`mapper/LastlegEventOutboxMapper.java`;多计划同柜只能在全柜完成后调用 `publishContainerCompleted`,按来源柜预订 ID 幂等发布一次,完成时刻与拍照时刻分离;事件模型见 `../common-event-contract/AGENTS.md` |
|
| 卸柜照片与海外卸柜完成 Outbox | `ReceivingContainerController.java`、`ReceivingContainerService.java`、`inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`、`entity/LastlegEventOutbox.java`、`mapper/LastlegEventOutboxMapper.java`;多计划同柜只能在全柜完成后调用 `publishContainerCompleted`,按来源柜预订 ID 幂等发布一次,完成时刻与拍照时刻分离;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||||
|
| 尾程暂扣状态回传 | `service/InboundOrderDetainedOutboxService.java`;暂扣与解除暂扣在订单更新事务内按 `sourceInboundId` 写入海外到国内 Outbox,国内按来源入仓单绑定更新 |
|
||||||
| 入仓后 SKU 库存余额、库位和流水 | `inventory/`;平台只读查询与非出库库存命令分别从 `inventory/query/controller/InventoryQueryController.java`、`inventory/command/controller/InventoryCommandController.java` 进入 |
|
| 入仓后 SKU 库存余额、库位和流水 | `inventory/`;平台只读查询与非出库库存命令分别从 `inventory/query/controller/InventoryQueryController.java`、`inventory/command/controller/InventoryCommandController.java` 进入 |
|
||||||
| 海外PDA盘点、同仓整托移库、托盘组拆换绑与换标现场作业 | `pda/stocktake/`、`pda/move/`、`palletops/`、`relabel/pda/`;版本化契约 `src/main/resources/openapi/overseas-pda-operations-v1.yaml`;全部范围取自PDA Token,不含出库和派送 |
|
| 海外PDA盘点、同仓整托移库、托盘组拆换绑与换标现场作业 | `pda/stocktake/`、`pda/move/`、`palletops/`、`relabel/pda/`;版本化契约 `src/main/resources/openapi/overseas-pda-operations-v1.yaml`;全部范围取自PDA Token,不含出库和派送 |
|
||||||
| 船节点接收与海外 PDA 可收货门禁 | `shipping/service/ContainerShippingNodeConsumer.java`、`shipping/entity/ContainerShippingGate.java`、`shipping/mapper/ContainerShippingGateMapper.java`;只单调接受实际标准节点,`PICKUP_READY` 及以后才允许对应柜进入 PDA 收货候选 |
|
| 船节点接收与海外 PDA 可收货门禁 | `shipping/service/ContainerShippingNodeConsumer.java`、`shipping/entity/ContainerShippingGate.java`、`shipping/mapper/ContainerShippingGateMapper.java`;只单调接受实际标准节点,`PICKUP_READY` 及以后才允许对应柜进入 PDA 收货候选 |
|
||||||
|
|||||||
+2
-1
@@ -101,7 +101,8 @@ public class LastlegOrderController {
|
|||||||
@RequestParam(required = false) String customerId,
|
@RequestParam(required = false) String customerId,
|
||||||
@PathVariable long id,
|
@PathVariable long id,
|
||||||
@Valid @RequestBody LastlegOrderRequests.SetDetained request) {
|
@Valid @RequestBody LastlegOrderRequests.SetDetained request) {
|
||||||
orderService.setDetained(platformScope(user, customerId), id, request, operatorName(user));
|
orderService.setDetained(platformScope(user, customerId), id, request,
|
||||||
|
String.valueOf(user.userId()), operatorName(user));
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -15,7 +15,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/** 至少一次投递海外卸柜完成事件;国内事件桥按 eventId 幂等接收。 */
|
/** 至少一次投递海外到国内业务事件;国内事件桥按 eventId 幂等接收。 */
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class InboundUnloadCompletedOutboxRelay {
|
public class InboundUnloadCompletedOutboxRelay {
|
||||||
@@ -44,7 +44,7 @@ public class InboundUnloadCompletedOutboxRelay {
|
|||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
repository.retry(row.getId(), workerId, LocalDateTime.now().plusSeconds(Math.max(retrySeconds, 1)),
|
repository.retry(row.getId(), workerId, LocalDateTime.now().plusSeconds(Math.max(retrySeconds, 1)),
|
||||||
exception.getMessage());
|
exception.getMessage());
|
||||||
log.warn("海外卸柜完成事件投递失败: eventKey={}, reason={}", row.getEventKey(), exception.getMessage());
|
log.warn("海外到国内事件投递失败: eventKey={}, reason={}", row.getEventKey(), exception.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ public class InboundUnloadCompletedOutboxRelay {
|
|||||||
com.baiyue.common.core.api.ApiResponse<EventBridgeClient.Acknowledgement> response) {
|
com.baiyue.common.core.api.ApiResponse<EventBridgeClient.Acknowledgement> response) {
|
||||||
if (response == null) throw new IllegalStateException("国内事件桥返回空响应");
|
if (response == null) throw new IllegalStateException("国内事件桥返回空响应");
|
||||||
if (!response.successful()) {
|
if (!response.successful()) {
|
||||||
throw new IllegalStateException("国内事件桥拒绝海外卸柜完成事件: code=" + response.code()
|
throw new IllegalStateException("国内事件桥拒绝海外事件: code=" + response.code()
|
||||||
+ ", message=" + response.message() + ", requestId=" + response.requestId());
|
+ ", message=" + response.message() + ", requestId=" + response.requestId());
|
||||||
}
|
}
|
||||||
if (response.data() == null) throw new IllegalStateException("国内事件桥成功响应缺少确认数据");
|
if (response.data() == null) throw new IllegalStateException("国内事件桥成功响应缺少确认数据");
|
||||||
|
|||||||
+7
-4
@@ -13,11 +13,14 @@ import java.util.List;
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public class UnloadCompletedOutboxRepository {
|
public class UnloadCompletedOutboxRepository {
|
||||||
|
private static final List<String> OVERSEAS_TO_CN_TYPES = List.of(
|
||||||
|
EventTypes.OVERSEAS_CONTAINER_UNLOADED,
|
||||||
|
EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED);
|
||||||
@Resource private LastlegEventOutboxMapper mapper;
|
@Resource private LastlegEventOutboxMapper mapper;
|
||||||
|
|
||||||
public List<LastlegEventOutbox> pending(int limit, LocalDateTime now, LocalDateTime expiredLeaseBefore) {
|
public List<LastlegEventOutbox> pending(int limit, LocalDateTime now, LocalDateTime expiredLeaseBefore) {
|
||||||
return mapper.selectList(new LambdaQueryWrapper<LastlegEventOutbox>()
|
return mapper.selectList(new LambdaQueryWrapper<LastlegEventOutbox>()
|
||||||
.eq(LastlegEventOutbox::getEventType, EventTypes.OVERSEAS_CONTAINER_UNLOADED)
|
.in(LastlegEventOutbox::getEventType, OVERSEAS_TO_CN_TYPES)
|
||||||
.and(status -> status.eq(LastlegEventOutbox::getStatus, "PENDING")
|
.and(status -> status.eq(LastlegEventOutbox::getStatus, "PENDING")
|
||||||
.and(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt)
|
.and(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt)
|
||||||
.or().le(LastlegEventOutbox::getNextRetryAt, now))
|
.or().le(LastlegEventOutbox::getNextRetryAt, now))
|
||||||
@@ -30,7 +33,7 @@ public class UnloadCompletedOutboxRepository {
|
|||||||
public boolean claim(long id, String worker, LocalDateTime now, LocalDateTime expiredLeaseBefore) {
|
public boolean claim(long id, String worker, LocalDateTime now, LocalDateTime expiredLeaseBefore) {
|
||||||
return mapper.update(null, new LambdaUpdateWrapper<LastlegEventOutbox>()
|
return mapper.update(null, new LambdaUpdateWrapper<LastlegEventOutbox>()
|
||||||
.eq(LastlegEventOutbox::getId, id)
|
.eq(LastlegEventOutbox::getId, id)
|
||||||
.eq(LastlegEventOutbox::getEventType, EventTypes.OVERSEAS_CONTAINER_UNLOADED)
|
.in(LastlegEventOutbox::getEventType, OVERSEAS_TO_CN_TYPES)
|
||||||
.and(status -> status.eq(LastlegEventOutbox::getStatus, "PENDING")
|
.and(status -> status.eq(LastlegEventOutbox::getStatus, "PENDING")
|
||||||
.and(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt)
|
.and(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt)
|
||||||
.or().le(LastlegEventOutbox::getNextRetryAt, now))
|
.or().le(LastlegEventOutbox::getNextRetryAt, now))
|
||||||
@@ -44,7 +47,7 @@ public class UnloadCompletedOutboxRepository {
|
|||||||
public LastlegEventOutbox claimed(long id, String worker) {
|
public LastlegEventOutbox claimed(long id, String worker) {
|
||||||
return mapper.selectOne(new LambdaQueryWrapper<LastlegEventOutbox>()
|
return mapper.selectOne(new LambdaQueryWrapper<LastlegEventOutbox>()
|
||||||
.eq(LastlegEventOutbox::getId, id)
|
.eq(LastlegEventOutbox::getId, id)
|
||||||
.eq(LastlegEventOutbox::getEventType, EventTypes.OVERSEAS_CONTAINER_UNLOADED)
|
.in(LastlegEventOutbox::getEventType, OVERSEAS_TO_CN_TYPES)
|
||||||
.eq(LastlegEventOutbox::getStatus, "PROCESSING")
|
.eq(LastlegEventOutbox::getStatus, "PROCESSING")
|
||||||
.eq(LastlegEventOutbox::getLockedBy, worker));
|
.eq(LastlegEventOutbox::getLockedBy, worker));
|
||||||
}
|
}
|
||||||
@@ -72,7 +75,7 @@ public class UnloadCompletedOutboxRepository {
|
|||||||
private LambdaUpdateWrapper<LastlegEventOutbox> claimedBase(long id, String worker) {
|
private LambdaUpdateWrapper<LastlegEventOutbox> claimedBase(long id, String worker) {
|
||||||
return new LambdaUpdateWrapper<LastlegEventOutbox>()
|
return new LambdaUpdateWrapper<LastlegEventOutbox>()
|
||||||
.eq(LastlegEventOutbox::getId, id)
|
.eq(LastlegEventOutbox::getId, id)
|
||||||
.eq(LastlegEventOutbox::getEventType, EventTypes.OVERSEAS_CONTAINER_UNLOADED)
|
.in(LastlegEventOutbox::getEventType, OVERSEAS_TO_CN_TYPES)
|
||||||
.eq(LastlegEventOutbox::getStatus, "PROCESSING")
|
.eq(LastlegEventOutbox::getStatus, "PROCESSING")
|
||||||
.eq(LastlegEventOutbox::getLockedBy, worker);
|
.eq(LastlegEventOutbox::getLockedBy, worker);
|
||||||
}
|
}
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.baiyue.lastleg.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
|
import com.baiyue.common.event.EventEnvelope;
|
||||||
|
import com.baiyue.common.event.EventIdentifiers;
|
||||||
|
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||||
|
import com.baiyue.common.event.EventPayloadHash;
|
||||||
|
import com.baiyue.common.event.EventTypes;
|
||||||
|
import com.baiyue.common.event.OverseasInboundOrderDetainedChangedV1;
|
||||||
|
import com.baiyue.common.event.RegionCode;
|
||||||
|
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||||
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
|
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
|
||||||
|
/** 在尾程业务事务内冻结按来源入仓单回传国内的暂扣状态事件。 */
|
||||||
|
@Service
|
||||||
|
public class InboundOrderDetainedOutboxService {
|
||||||
|
@Resource private LastlegEventOutboxMapper mapper;
|
||||||
|
@Resource private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public void publish(LastlegOrder order, boolean detained, String reason, String operatorId,
|
||||||
|
String operatorName, Instant changedAt, long changeVersion) {
|
||||||
|
requireText(order.getSourceInboundId(), "sourceInboundId");
|
||||||
|
requireText(order.getInboundCode(), "inboundCode");
|
||||||
|
requireText(order.getCustomerId(), "customerId");
|
||||||
|
var payload = new OverseasInboundOrderDetainedChangedV1(order.getSourceInboundId(),
|
||||||
|
order.getInboundCode(), String.valueOf(order.getId()), order.getOrderNo(), detained,
|
||||||
|
reason, operatorId, operatorName, changedAt);
|
||||||
|
JsonNode payloadNode = objectMapper.valueToTree(payload);
|
||||||
|
String eventKey = EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED + ':'
|
||||||
|
+ order.getSourceInboundId() + ':' + changeVersion;
|
||||||
|
EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(),
|
||||||
|
EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED, 1, changedAt,
|
||||||
|
RegionCode.OVERSEAS, RegionCode.CN, order.getCustomerId(), "INBOUND_ORDER",
|
||||||
|
order.getSourceInboundId(), changeVersion, 1L, EventIdentifiers.newUuid(),
|
||||||
|
eventKey, order.getSourceEventId(), payloadNode,
|
||||||
|
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode))).requireValid();
|
||||||
|
|
||||||
|
LastlegEventOutbox event = new LastlegEventOutbox();
|
||||||
|
event.setId(IdWorker.getId());
|
||||||
|
event.setCustomerId(order.getCustomerId());
|
||||||
|
event.setAggregateType(envelope.aggregateType());
|
||||||
|
event.setAggregateId(envelope.aggregateId());
|
||||||
|
event.setEventType(envelope.eventType());
|
||||||
|
event.setEventVersion(envelope.eventVersion());
|
||||||
|
event.setEventKey(eventKey);
|
||||||
|
event.setPayloadJson(json(envelope));
|
||||||
|
event.setStatus("PENDING");
|
||||||
|
event.setRetryCount(0);
|
||||||
|
event.setCreatedAt(LocalDateTime.ofInstant(changedAt, ZoneOffset.UTC));
|
||||||
|
mapper.insert(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireText(String value, String field) {
|
||||||
|
if (value == null || value.isBlank()) throw new IllegalStateException(field + " is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String json(Object value) {
|
||||||
|
try { return objectMapper.writeValueAsString(value); }
|
||||||
|
catch (JsonProcessingException exception) {
|
||||||
|
throw new IllegalStateException("Cannot serialize inbound detained event", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -54,6 +55,7 @@ public class LastlegOrderService {
|
|||||||
@Resource private LastlegOrderOperationLogMapper operationLogMapper;
|
@Resource private LastlegOrderOperationLogMapper operationLogMapper;
|
||||||
@Resource private InboundPlanCartonMapper cartonMapper;
|
@Resource private InboundPlanCartonMapper cartonMapper;
|
||||||
@Resource private InventoryCommandService inventory;
|
@Resource private InventoryCommandService inventory;
|
||||||
|
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
|
||||||
|
|
||||||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
||||||
Page<LastlegOrder> page = orderMapper.selectPage(
|
Page<LastlegOrder> page = orderMapper.selectPage(
|
||||||
@@ -123,6 +125,12 @@ public class LastlegOrderService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained request,
|
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained request,
|
||||||
String operatorName) {
|
String operatorName) {
|
||||||
|
setDetained(customerId, orderId, request, null, operatorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained request,
|
||||||
|
String operatorId, String operatorName) {
|
||||||
LastlegOrder current = findOwned(customerId, orderId);
|
LastlegOrder current = findOwned(customerId, orderId);
|
||||||
LastlegOrder order = orderMapper.lockById(current.getCustomerId(), orderId);
|
LastlegOrder order = orderMapper.lockById(current.getCustomerId(), orderId);
|
||||||
if (order == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tail-leg order not found");
|
if (order == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tail-leg order not found");
|
||||||
@@ -149,6 +157,10 @@ public class LastlegOrderService {
|
|||||||
operation.setResult(request.remark().trim());
|
operation.setResult(request.remark().trim());
|
||||||
operation.setCreatedAt(now);
|
operation.setCreatedAt(now);
|
||||||
operationLogMapper.insert(operation);
|
operationLogMapper.insert(operation);
|
||||||
|
if (StringUtils.hasText(order.getSourceInboundId())) {
|
||||||
|
detainedOutbox.publish(order, detained, request.remark().trim(), operatorId, operatorName,
|
||||||
|
now.toInstant(ZoneOffset.UTC), operation.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
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.ArgumentMatchers.isNull;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -43,6 +45,7 @@ class LastlegOrderServiceTest {
|
|||||||
@Mock LastlegOrderOperationLogMapper operations;
|
@Mock LastlegOrderOperationLogMapper operations;
|
||||||
@Mock InboundPlanCartonMapper cartons;
|
@Mock InboundPlanCartonMapper cartons;
|
||||||
@Mock InventoryCommandService inventory;
|
@Mock InventoryCommandService inventory;
|
||||||
|
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
||||||
LastlegOrderService service;
|
LastlegOrderService service;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -55,6 +58,7 @@ class LastlegOrderServiceTest {
|
|||||||
ReflectionTestUtils.setField(service, "operationLogMapper", operations);
|
ReflectionTestUtils.setField(service, "operationLogMapper", operations);
|
||||||
ReflectionTestUtils.setField(service, "cartonMapper", cartons);
|
ReflectionTestUtils.setField(service, "cartonMapper", cartons);
|
||||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||||
|
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -228,6 +232,7 @@ class LastlegOrderServiceTest {
|
|||||||
verify(operations).insert(operation.capture());
|
verify(operations).insert(operation.capture());
|
||||||
assertEquals("暂扣", operation.getValue().getOperationNode());
|
assertEquals("暂扣", operation.getValue().getOperationNode());
|
||||||
assertEquals("等待质检", operation.getValue().getResult());
|
assertEquals("等待质检", operation.getValue().getResult());
|
||||||
|
verify(detainedOutbox).publish(eq(order), eq(true), eq("等待质检"), isNull(), eq("operator"), any(), anyLong());
|
||||||
verify(inventory, never()).releaseInboundOrderHold(any(), any(), anyLong(), any(), any());
|
verify(inventory, never()).releaseInboundOrderHold(any(), any(), anyLong(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +248,7 @@ class LastlegOrderServiceTest {
|
|||||||
|
|
||||||
verify(inventory).releaseInboundOrderHold("100", "operator", 11L, "LM-1", "质检通过");
|
verify(inventory).releaseInboundOrderHold("100", "operator", 11L, "LM-1", "质检通过");
|
||||||
assertEquals(false, order.getDetained());
|
assertEquals(false, order.getDetained());
|
||||||
|
verify(detainedOutbox).publish(eq(order), eq(false), eq("质检通过"), isNull(), eq("operator"), any(), anyLong());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LastlegOrder order(long id, String customerId, String status, String inboundOrderNo) {
|
private static LastlegOrder order(long id, String customerId, String status, String inboundOrderNo) {
|
||||||
@@ -251,6 +257,9 @@ class LastlegOrderServiceTest {
|
|||||||
order.setCustomerId(customerId);
|
order.setCustomerId(customerId);
|
||||||
order.setCurrentStatus(status);
|
order.setCurrentStatus(status);
|
||||||
order.setInboundOrderNo(inboundOrderNo);
|
order.setInboundOrderNo(inboundOrderNo);
|
||||||
|
order.setOrderNo("LM-" + id);
|
||||||
|
order.setSourceInboundId(String.valueOf(1000 + id));
|
||||||
|
order.setInboundCode("IN-" + id);
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user