尾程暂扣回传
This commit is contained in:
+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.exception.BizException;
|
||||
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.OverseasInboundOrderDetainedConsumer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -20,14 +21,17 @@ import java.security.MessageDigest;
|
||||
@RequiredArgsConstructor
|
||||
public class InternalOverseasContainerEventController {
|
||||
private final OverseasContainerEventConsumer consumer;
|
||||
private final OverseasInboundOrderDetainedConsumer detainedConsumer;
|
||||
@Value("${baiyue.security.internal-service-token}") private String expectedToken;
|
||||
|
||||
@PostMapping("/internal/firstleg/v1/container-events")
|
||||
public ApiResponse<ContainerBookingEntity> receive(
|
||||
public ApiResponse<Object> receive(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestBody EventEnvelope event) {
|
||||
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) {
|
||||
|
||||
@@ -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 Boolean holdFlag;
|
||||
private String currentHoldReason;
|
||||
private Long lastOverseasHoldVersion;
|
||||
private Integer totalPackageCount;
|
||||
private BigDecimal actualGrossWeightKg;
|
||||
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.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
/** 入库聚合根条件更新 Mapper,所有推进均同时校验租户、前置状态和版本。 */
|
||||
@Mapper
|
||||
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 fl_inbound_order
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user