尾程暂扣回传

This commit is contained in:
2026-08-31 15:22:32 +08:00
parent a4189a0074
commit a213be67c5
22 changed files with 438 additions and 17 deletions
@@ -101,7 +101,8 @@ public class LastlegOrderController {
@RequestParam(required = false) String customerId,
@PathVariable long id,
@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);
}
@@ -15,7 +15,7 @@ import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.UUID;
/** 至少一次投递海外卸柜完成事件;国内事件桥按 eventId 幂等接收。 */
/** 至少一次投递海外到国内业务事件;国内事件桥按 eventId 幂等接收。 */
@Component
@Slf4j
public class InboundUnloadCompletedOutboxRelay {
@@ -44,7 +44,7 @@ public class InboundUnloadCompletedOutboxRelay {
} catch (Exception exception) {
repository.retry(row.getId(), workerId, LocalDateTime.now().plusSeconds(Math.max(retrySeconds, 1)),
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) {
if (response == null) throw new IllegalStateException("国内事件桥返回空响应");
if (!response.successful()) {
throw new IllegalStateException("国内事件桥拒绝海外卸柜完成事件: code=" + response.code()
throw new IllegalStateException("国内事件桥拒绝海外事件: code=" + response.code()
+ ", message=" + response.message() + ", requestId=" + response.requestId());
}
if (response.data() == null) throw new IllegalStateException("国内事件桥成功响应缺少确认数据");
@@ -13,11 +13,14 @@ import java.util.List;
@Repository
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;
public List<LastlegEventOutbox> pending(int limit, LocalDateTime now, LocalDateTime expiredLeaseBefore) {
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(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt)
.or().le(LastlegEventOutbox::getNextRetryAt, now))
@@ -30,7 +33,7 @@ public class UnloadCompletedOutboxRepository {
public boolean claim(long id, String worker, LocalDateTime now, LocalDateTime expiredLeaseBefore) {
return mapper.update(null, new LambdaUpdateWrapper<LastlegEventOutbox>()
.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(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt)
.or().le(LastlegEventOutbox::getNextRetryAt, now))
@@ -44,7 +47,7 @@ public class UnloadCompletedOutboxRepository {
public LastlegEventOutbox claimed(long id, String worker) {
return mapper.selectOne(new LambdaQueryWrapper<LastlegEventOutbox>()
.eq(LastlegEventOutbox::getId, id)
.eq(LastlegEventOutbox::getEventType, EventTypes.OVERSEAS_CONTAINER_UNLOADED)
.in(LastlegEventOutbox::getEventType, OVERSEAS_TO_CN_TYPES)
.eq(LastlegEventOutbox::getStatus, "PROCESSING")
.eq(LastlegEventOutbox::getLockedBy, worker));
}
@@ -72,7 +75,7 @@ public class UnloadCompletedOutboxRepository {
private LambdaUpdateWrapper<LastlegEventOutbox> claimedBase(long id, String worker) {
return new LambdaUpdateWrapper<LastlegEventOutbox>()
.eq(LastlegEventOutbox::getId, id)
.eq(LastlegEventOutbox::getEventType, EventTypes.OVERSEAS_CONTAINER_UNLOADED)
.in(LastlegEventOutbox::getEventType, OVERSEAS_TO_CN_TYPES)
.eq(LastlegEventOutbox::getStatus, "PROCESSING")
.eq(LastlegEventOutbox::getLockedBy, worker);
}
@@ -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.OutputStream;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -54,6 +55,7 @@ public class LastlegOrderService {
@Resource private LastlegOrderOperationLogMapper operationLogMapper;
@Resource private InboundPlanCartonMapper cartonMapper;
@Resource private InventoryCommandService inventory;
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
Page<LastlegOrder> page = orderMapper.selectPage(
@@ -123,6 +125,12 @@ public class LastlegOrderService {
@Transactional
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained request,
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 order = orderMapper.lockById(current.getCustomerId(), orderId);
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.setCreatedAt(now);
operationLogMapper.insert(operation);
if (StringUtils.hasText(order.getSourceInboundId())) {
detainedOutbox.publish(order, detained, request.remark().trim(), operatorId, operatorName,
now.toInstant(ZoneOffset.UTC), operation.getId());
}
}
@Transactional