Merge origin/dev and resolve backend conflicts

This commit is contained in:
2026-09-08 20:50:28 +08:00
342 changed files with 13161 additions and 2296 deletions
@@ -38,11 +38,13 @@ public class InboundOrderDetainedOutboxService {
JsonNode payloadNode = objectMapper.valueToTree(payload);
String eventKey = EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED + ':'
+ order.getSourceInboundId() + ':' + changeVersion;
String causationId = EventIdentifiers.isSupported(order.getSourceEventId())
? order.getSourceEventId() : null;
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,
eventKey, causationId, payloadNode,
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode))).requireValid();
LastlegEventOutbox event = new LastlegEventOutbox();
@@ -0,0 +1,240 @@
package com.baiyue.lastleg.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderCommandRecord;
import com.baiyue.lastleg.entity.LastlegOrderHoldHistory;
import com.baiyue.lastleg.entity.LastlegOrderManualNode;
import com.baiyue.lastleg.entity.LastlegOrderManualNodeHistory;
import com.baiyue.lastleg.entity.LastlegOrderOperationLog;
import com.baiyue.lastleg.mapper.LastlegOrderCommandRecordMapper;
import com.baiyue.lastleg.mapper.LastlegOrderHoldHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
public class LastlegOrderCommandService {
private static final String HOLD = "HOLD";
private static final String MANUAL_NODES = "MANUAL_NODES";
private static final List<String> NODE_CODES = List.of(
"DOMESTIC_CUSTOMS_COMPLETED", "ESTIMATED_DEPARTURE", "ACTUAL_DEPARTURE", "ESTIMATED_ARRIVAL",
"OVERSEAS_CUSTOMS_COMPLETED", "ACTUAL_ARRIVAL_WAITING_UNLOAD", "PICKUP_APPOINTED", "UNLOADED");
@Resource private LastlegOrderMapper orders;
@Resource private LastlegOrderCommandRecordMapper commands;
@Resource private LastlegOrderHoldHistoryMapper holdHistory;
@Resource private LastlegOrderManualNodeMapper manualNodes;
@Resource private LastlegOrderManualNodeHistoryMapper manualNodeHistory;
@Resource private LastlegOrderOperationLogMapper operationLogs;
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
@Resource private ObjectMapper objectMapper;
@Transactional
public LastlegOrderCommandDtos.HoldResponse hold(String requestedCustomerId, long orderId,
String idempotencyKey,
LastlegOrderCommandDtos.HoldRequest request,
String operatorId, String operatorName) {
String key = key(idempotencyKey);
LastlegOrder order = lockOwned(requestedCustomerId, orderId);
String hash = hash(request);
LastlegOrderCommandRecord replay = commands.find(order.getCustomerId(), orderId, HOLD, operatorId, key);
if (replay != null) return replay(replay, hash, LastlegOrderCommandDtos.HoldResponse.class);
int version = version(order);
if (request.expectedVersion() != version) throw conflict("尾程订单版本已变化,请刷新后重试");
boolean held = request.held();
if (Boolean.TRUE.equals(order.getDetained()) == held) {
throw conflict(held ? "尾程订单已暂扣" : "尾程订单未处于暂扣状态");
}
if (held && (positive(order.getPickedPackageCount()) || positive(order.getShippedPackageCount())
|| text(order.getOutboundOrderNo()) != null)) {
throw conflict("尾程订单已进入出库流程,不能新增暂扣");
}
LocalDateTime now = LocalDateTime.now();
boolean chargeable = held && request.chargeable();
String reason = request.reason().trim();
if (orders.updateHold(order.getCustomerId(), orderId, held, chargeable, reason, version, now) != 1) {
throw conflict("尾程订单版本已变化,请刷新后重试");
}
int nextVersion = version + 1;
LastlegOrderCommandDtos.HoldResponse response = new LastlegOrderCommandDtos.HoldResponse(
orderId, held, chargeable, held ? reason : null, nextVersion);
LastlegOrderHoldHistory history = new LastlegOrderHoldHistory();
history.setId(IdWorker.getId()); history.setCustomerId(order.getCustomerId()); history.setOrderId(orderId);
history.setHeld(held); history.setChargeable(chargeable); history.setReason(reason);
history.setOrderVersion(nextVersion); history.setOperatorId(operatorId); history.setOperatorName(operatorName);
history.setIdempotencyKey(key); history.setCreatedAt(now); holdHistory.insert(history);
operation(order, held ? "暂扣" : "解除暂扣", operatorName,
(chargeable ? "计费;" : "不计费;") + reason, now);
saveCommand(order, HOLD, operatorId, key, hash, null, response, nextVersion, now);
order.setDetained(held);
order.setHoldChargeable(chargeable);
order.setHoldReason(held ? reason : null);
order.setVersion(nextVersion);
if (text(order.getSourceInboundId()) != null) {
detainedOutbox.publish(order, held, reason, operatorId, operatorName,
now.toInstant(ZoneOffset.UTC), history.getId());
}
return response;
}
@Transactional
public LastlegOrderCommandDtos.ManualNodesResponse saveManualNodes(String requestedCustomerId, long orderId,
String idempotencyKey,
LastlegOrderCommandDtos.ManualNodesRequest request,
String operatorId, String operatorName) {
String key = key(idempotencyKey);
LastlegOrder order = lockOwned(requestedCustomerId, orderId);
List<LastlegOrderCommandDtos.ManualNode> normalized = normalizeNodes(request.nodes());
String hash = hash(new LastlegOrderCommandDtos.ManualNodesRequest(request.expectedVersion(), normalized));
LastlegOrderCommandRecord replay = commands.find(order.getCustomerId(), orderId, MANUAL_NODES, operatorId, key);
if (replay != null) return replay(replay, hash, LastlegOrderCommandDtos.ManualNodesResponse.class);
int version = version(order);
if (request.expectedVersion() != version) throw conflict("尾程订单版本已变化,请刷新后重试");
Map<String, LocalDateTime> values = new HashMap<>();
normalized.forEach(node -> values.put(node.nodeCode(), node.nodeAt()));
LocalDateTime now = LocalDateTime.now();
if (orders.updateManualNodes(order.getCustomerId(), orderId,
values.get("ESTIMATED_ARRIVAL"), values.get("ACTUAL_ARRIVAL_WAITING_UNLOAD"),
null, version, now) != 1) {
throw conflict("尾程订单版本已变化,请刷新后重试");
}
int nextVersion = version + 1;
manualNodes.deleteCurrent(order.getCustomerId(), orderId);
List<LastlegOrderCommandDtos.ManualNodeView> views = new ArrayList<>(normalized.size());
for (LastlegOrderCommandDtos.ManualNode node : normalized) {
LastlegOrderManualNode entity = new LastlegOrderManualNode();
entity.setId(IdWorker.getId()); entity.setCustomerId(order.getCustomerId()); entity.setOrderId(orderId);
entity.setNodeCode(node.nodeCode()); entity.setNodeAt(node.nodeAt()); entity.setSource("MANUAL");
entity.setOrderVersion(nextVersion); entity.setUpdatedBy(operatorId); entity.setUpdatedAt(now);
manualNodes.insert(entity);
views.add(new LastlegOrderCommandDtos.ManualNodeView(node.nodeCode(), node.nodeAt(), "MANUAL"));
}
LastlegOrderCommandDtos.ManualNodesResponse response = new LastlegOrderCommandDtos.ManualNodesResponse(
orderId, true, List.copyOf(views), nextVersion);
LastlegOrderManualNodeHistory history = new LastlegOrderManualNodeHistory();
history.setId(IdWorker.getId()); history.setCustomerId(order.getCustomerId()); history.setOrderId(orderId);
history.setNodesJson(json(views)); history.setOrderVersion(nextVersion); history.setOperatorId(operatorId);
history.setOperatorName(operatorName); history.setIdempotencyKey(key); history.setCreatedAt(now);
manualNodeHistory.insert(history);
operation(order, "人工计划节点", operatorName, "保存" + views.size() + "个人工节点", now);
saveCommand(order, MANUAL_NODES, operatorId, key, hash, null, response, nextVersion, now);
return response;
}
@Transactional(readOnly = true)
public LastlegOrderCommandDtos.ManualNodesResponse manualNodes(String requestedCustomerId, long orderId) {
LastlegOrder order = findOwned(requestedCustomerId, orderId);
List<LastlegOrderCommandDtos.ManualNodeView> nodes = manualNodes.selectList(
new LambdaQueryWrapper<LastlegOrderManualNode>()
.eq(LastlegOrderManualNode::getCustomerId, order.getCustomerId())
.eq(LastlegOrderManualNode::getOrderId, orderId)
.orderByAsc(LastlegOrderManualNode::getNodeCode))
.stream().map(node -> new LastlegOrderCommandDtos.ManualNodeView(
node.getNodeCode(), node.getNodeAt(), node.getSource()))
.sorted(java.util.Comparator.comparingInt(node -> NODE_CODES.indexOf(node.nodeCode()))).toList();
return new LastlegOrderCommandDtos.ManualNodesResponse(orderId,
Boolean.TRUE.equals(order.getManualNodeOverride()), nodes, version(order));
}
private List<LastlegOrderCommandDtos.ManualNode> normalizeNodes(List<LastlegOrderCommandDtos.ManualNode> nodes) {
Set<String> seen = new HashSet<>();
boolean anyTime = false;
List<LastlegOrderCommandDtos.ManualNode> result = new ArrayList<>();
for (LastlegOrderCommandDtos.ManualNode node : nodes) {
String code = node.nodeCode().trim().toUpperCase();
if (!NODE_CODES.contains(code)) throw badRequest("不支持的人工展示节点:" + code);
if (!seen.add(code)) throw badRequest("人工节点不能重复:" + code);
result.add(new LastlegOrderCommandDtos.ManualNode(code, node.nodeAt()));
anyTime |= node.nodeAt() != null;
}
if (!seen.equals(Set.copyOf(NODE_CODES))) throw badRequest("必须提交完整8行人工时间快照");
if (!anyTime) throw badRequest("人工时间快照至少填写一个节点时间");
result.sort(java.util.Comparator.comparingInt(node -> NODE_CODES.indexOf(node.nodeCode())));
return List.copyOf(result);
}
private LastlegOrder lockOwned(String requestedCustomerId, long orderId) {
LastlegOrder found = findOwned(requestedCustomerId, orderId);
LastlegOrder locked = orders.lockById(found.getCustomerId(), orderId);
if (locked == null) throw notFound();
return locked;
}
private LastlegOrder findOwned(String requestedCustomerId, long orderId) {
LastlegOrder order = orders.selectById(orderId);
if (order == null || (text(requestedCustomerId) != null
&& !requestedCustomerId.trim().equals(order.getCustomerId()))) throw notFound();
return order;
}
private void operation(LastlegOrder order, String node, String operatorName, String result, LocalDateTime now) {
LastlegOrderOperationLog log = new LastlegOrderOperationLog();
log.setId(IdWorker.getId()); log.setCustomerId(order.getCustomerId()); log.setOrderId(order.getId());
log.setOperationNode(node); log.setOperatorName(operatorName); log.setResult(result); log.setCreatedAt(now);
operationLogs.insert(log);
}
private void saveCommand(LastlegOrder order, String action, String operatorId, String key, String hash,
String resultRef, Object response, int resultVersion, LocalDateTime now) {
LastlegOrderCommandRecord record = new LastlegOrderCommandRecord();
record.setId(IdWorker.getId()); record.setCustomerId(order.getCustomerId()); record.setOrderId(order.getId());
record.setAction(action); record.setOperatorId(operatorId); record.setIdempotencyKey(key);
record.setRequestHash(hash); record.setResultRef(resultRef); record.setResultJson(json(response));
record.setResultVersion(resultVersion); record.setCreatedAt(now); commands.insert(record);
}
private <T> T replay(LastlegOrderCommandRecord record, String hash, Class<T> type) {
if (!MessageDigest.isEqual(record.getRequestHash().getBytes(StandardCharsets.UTF_8),
hash.getBytes(StandardCharsets.UTF_8))) throw conflict("同一幂等键不能用于不同请求");
try { return objectMapper.readValue(record.getResultJson(), type); }
catch (JsonProcessingException exception) { throw new IllegalStateException("无法读取已保存命令结果", exception); }
}
private String hash(Object request) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(objectMapper.writeValueAsBytes(request)));
} catch (NoSuchAlgorithmException | JsonProcessingException exception) {
throw new IllegalStateException("无法生成命令摘要", exception);
}
}
private String json(Object value) {
try { return objectMapper.writeValueAsString(value); }
catch (JsonProcessingException exception) { throw new IllegalStateException("无法保存命令结果", exception); }
}
private String key(String value) {
String key = text(value);
if (key == null || key.length() > 128) throw badRequest("Idempotency-Key 必须为1至128个字符");
return key;
}
private int version(LastlegOrder order) { return order.getVersion() == null ? 0 : order.getVersion(); }
private boolean positive(Integer value) { return value != null && value > 0; }
private String text(String value) { return value == null || value.isBlank() ? null : value.trim(); }
private ResponseStatusException notFound() { return new ResponseStatusException(HttpStatus.NOT_FOUND, "尾程订单不存在"); }
private ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
private ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
}
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.event.ContainerNumbers;
import com.baiyue.lastleg.dto.LastlegOrderDetailResponse;
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos;
import com.baiyue.lastleg.dto.LastlegOrderRequests;
import com.baiyue.lastleg.dto.LastlegOrderSummary;
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
@@ -15,6 +16,9 @@ import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.entity.LastlegOrderFee;
import com.baiyue.lastleg.entity.LastlegOrderOperationLog;
import com.baiyue.lastleg.entity.LastlegOrderManualNode;
import com.baiyue.lastleg.entity.ReceivingWarehouseOperation;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
@@ -23,6 +27,8 @@ import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper;
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
import com.baiyue.lastleg.model.LastlegOrderType;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
@@ -37,6 +43,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -50,16 +57,22 @@ public class LastlegOrderService {
public static final String INBOUND_PLANNED = "INBOUND_PLANNED";
public static final String PALLET_MERGE_REQUIRED = "REQUIRED";
public static final String PALLET_MERGED = "MERGED";
private static final List<String> MANUAL_NODE_CODES = List.of(
"DOMESTIC_CUSTOMS_COMPLETED", "ESTIMATED_DEPARTURE", "ACTUAL_DEPARTURE", "ESTIMATED_ARRIVAL",
"OVERSEAS_CUSTOMS_COMPLETED", "ACTUAL_ARRIVAL_WAITING_UNLOAD", "PICKUP_APPOINTED", "UNLOADED");
@Resource private LastlegOrderMapper orderMapper;
@Resource private LastlegOrderDetailMapper detailMapper;
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
@Resource private LastlegOrderFeeMapper feeMapper;
@Resource private LastlegOrderOperationLogMapper operationLogMapper;
@Resource private LastlegOrderManualNodeMapper manualNodeMapper;
@Resource private InboundPlanCartonMapper cartonMapper;
@Resource private FbaWarehouseValidator fbaWarehouseValidator;
@Resource private InventoryCommandService inventory;
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
@Resource private ReceivingWarehouseOperationMapper receivingWarehouseOperations;
@Resource private WarehouseResourceGateway warehouseResourceGateway;
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
Page<LastlegOrder> page = orderMapper.selectPage(
@@ -80,7 +93,7 @@ public class LastlegOrderService {
long total = orderMapper.countContainerProgress(query);
List<ContainerPlanProgressSummary> records = total == 0 ? List.of()
: orderMapper.pageContainerProgress(query, (query.page() - 1) * query.size(), query.size())
.stream().map(this::requireFrozenContainer).toList();
.stream().map(this::containerSummaryForRead).toList();
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
return new PageResult<>(records, query.page(), query.size(), total, pages);
}
@@ -95,17 +108,131 @@ public class LastlegOrderService {
if (summary == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
}
return new ContainerPlanProgressDetail(requireFrozenContainer(summary),
return new ContainerPlanProgressDetail(containerSummaryForRead(summary),
orderMapper.findContainerProgressItems(bookingId));
}
private ContainerPlanProgressSummary requireFrozenContainer(ContainerPlanProgressSummary summary) {
try {
ContainerNumbers.requireCanonical(summary.containerNo());
return summary;
} catch (IllegalArgumentException exception) {
@Transactional
public ContainerPlanProgressSummary changeReceivingWarehouse(String sourceContainerBookingId,
String idempotencyKey, LastlegOrderRequests.ChangeReceivingWarehouse request, String operatorName) {
String bookingId = normalizeBookingId(sourceContainerBookingId);
String key = normalizeIdempotencyKey(idempotencyKey);
String requestHash = hash(bookingId + "\n" + request.overseasWarehouseId() + "\n"
+ request.expectedVersion() + "\n" + Objects.toString(request.remark(), ""));
ReceivingWarehouseOperation replay = receivingWarehouseOperations.findByKey(key);
if (replay != null) {
if (!bookingId.equals(replay.getSourceContainerBookingId()) || !requestHash.equals(replay.getRequestHash())) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Idempotency-Key 已用于其他接柜仓修改请求");
}
return requireFrozenContainer(orderMapper.findContainerProgress(bookingId));
}
List<LastlegOrder> orders = orderMapper.lockContainerOrders(bookingId);
if (orders.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
int version = requireSingleAssignmentVersion(orders);
if (version != request.expectedVersion()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "接柜仓分配版本已变化,请刷新后重试");
}
if (orderMapper.countReceivingSessions(bookingId) > 0) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"来源柜真实集装箱号缺失、格式无效或存在漂移", exception);
"海外PDA已建立接柜会话,接柜仓已永久锁定");
}
WarehouseResourceGateway.WarehouseReference warehouse =
warehouseResourceGateway.requireActiveSharedWarehouse(request.overseasWarehouseId());
for (LastlegOrder order : orders) {
inventory.rebindReceivingWarehouse(order.getCustomerId(), order.getId(), warehouse.warehouseId(),
warehouse.warehouseCode(), warehouse.warehouseName(), operatorName, key);
}
int updated = orderMapper.updateContainerReceivingWarehouse(bookingId,
Long.toString(warehouse.warehouseId()), warehouse.warehouseCode(), warehouse.warehouseName(), version);
if (updated != orders.size()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内接柜仓分配发生并发变化");
}
orderMapper.updateContainerPlanReceivingWarehouse(bookingId, warehouse.warehouseId());
ReceivingWarehouseOperation operation = new ReceivingWarehouseOperation();
operation.setId(IdWorker.getId()); operation.setSourceContainerBookingId(bookingId);
operation.setIdempotencyKey(key); operation.setRequestHash(requestHash);
operation.setResultVersion(version + 1); operation.setOperatorName(operatorName);
operation.setRemark(request.remark() == null ? null : request.remark().trim());
operation.setCreatedAt(LocalDateTime.now()); receivingWarehouseOperations.insert(operation);
return requireFrozenContainer(orderMapper.findContainerProgress(bookingId));
}
private ContainerPlanProgressSummary requireFrozenContainer(ContainerPlanProgressSummary summary) {
if (summary == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
String issue = containerSnapshotIssue(summary);
if (issue != null) throw new ResponseStatusException(HttpStatus.CONFLICT, issue);
return summary;
}
private ContainerPlanProgressSummary containerSummaryForRead(ContainerPlanProgressSummary summary) {
String issue = containerSnapshotIssue(summary);
if (issue == null) return summary;
boolean invalidContainer = !hasCanonicalContainerNo(summary.containerNo());
return new ContainerPlanProgressSummary(summary.sourceContainerBookingId(), summary.containerNo(),
summary.overseasWarehouseId(), summary.overseasWarehouseCode(), summary.overseasWarehouseName(),
summary.assignmentVersion(), invalidContainer ? false : summary.canChangeReceivingWarehouse(),
invalidContainer ? issue : summary.receivingWarehouseLockReason(), summary.totalOrderCount(),
summary.inboundCodeCount(), summary.missingInboundCodeCount(), summary.planCount(),
summary.plannedInboundCodeCount(), summary.pendingPlanCount(), summary.plannedArrivalAt(),
false, issue);
}
private String containerSnapshotIssue(ContainerPlanProgressSummary summary) {
if (!hasCanonicalContainerNo(summary.containerNo())) {
return "来源柜真实集装箱号缺失、格式无效或存在漂移";
}
if (!StringUtils.hasText(summary.overseasWarehouseId())
|| !StringUtils.hasText(summary.overseasWarehouseCode())
|| !StringUtils.hasText(summary.overseasWarehouseName()) || summary.assignmentVersion() < 0) {
return "柜内接柜仓业务快照不一致";
}
return null;
}
private boolean hasCanonicalContainerNo(String containerNo) {
try {
ContainerNumbers.requireCanonical(containerNo);
return true;
} catch (IllegalArgumentException exception) {
return false;
}
}
private String normalizeBookingId(String value) {
String result = value == null ? null : value.trim();
if (!StringUtils.hasText(result) || result.length() > 128) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订ID不能为空");
}
return result;
}
private String normalizeIdempotencyKey(String value) {
String result = value == null ? null : value.trim();
if (!StringUtils.hasText(result) || result.length() > 255) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Idempotency-Key 必填且不得超过255字符");
}
return result;
}
private int requireSingleAssignmentVersion(List<LastlegOrder> orders) {
Integer version = orders.get(0).getReceivingWarehouseVersion();
if (version == null || orders.stream().anyMatch(order -> !Objects.equals(version,
order.getReceivingWarehouseVersion()) || !StringUtils.hasText(order.getOverseasWarehouseId())
|| !StringUtils.hasText(order.getOverseasWarehouseCode())
|| !StringUtils.hasText(order.getOverseasWarehouseName()))) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内接柜仓分配事实不一致");
}
return version;
}
private String hash(String value) {
try {
return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(value.getBytes(StandardCharsets.UTF_8)));
} catch (Exception exception) {
throw new IllegalStateException(exception);
}
}
@@ -127,7 +254,15 @@ public class LastlegOrderService {
.eq(LastlegOrderOperationLog::getCustomerId, order.getCustomerId())
.eq(LastlegOrderOperationLog::getOrderId, orderId)
.orderByAsc(LastlegOrderOperationLog::getCreatedAt));
return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId())), cargo, history.stream()
List<LastlegOrderCommandDtos.ManualNodeView> manualNodes = manualNodeMapper.selectList(
new LambdaQueryWrapper<LastlegOrderManualNode>()
.eq(LastlegOrderManualNode::getCustomerId, order.getCustomerId())
.eq(LastlegOrderManualNode::getOrderId, orderId)
.orderByAsc(LastlegOrderManualNode::getId)).stream()
.map(value -> new LastlegOrderCommandDtos.ManualNodeView(
value.getNodeCode(), value.getNodeAt(), value.getSource()))
.sorted(java.util.Comparator.comparingInt(value -> MANUAL_NODE_CODES.indexOf(value.nodeCode()))).toList();
return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId())), manualNodes, cargo, history.stream()
.map(value -> new LastlegOrderDetailResponse.History(value.getId(), value.getFromStatus(),
value.getToStatus(), value.getRemark(), value.getCreatedAt())).toList(), fees, operations.stream()
.map(value -> new LastlegOrderDetailResponse.Operation(value.getId(), value.getOperationNode(),
@@ -358,7 +493,7 @@ public class LastlegOrderService {
for (LastlegOrder order : orders) {
String line = String.join(",", List.of(
csv(order.getId()), csv(order.getOrderNo()), csv(LastlegOrderType.labelOf(order.getOrderType())),
csv(order.getInboundCode()),
csv(displayInboundCode(order)),
csv(order.getSourceOrderNo()), csv(order.getCustomerName()), csv(order.getOverseasWarehouseName()),
csv(order.getTransferWarehouseName()), csv(order.getTransferWarehouseCode()), csv(order.getFbaWarehouseCode()),
csv(order.getContainerNo()), csv(order.getContainerStatus()), csv(order.getAbnormal()), csv(order.getDetained()),
@@ -380,6 +515,7 @@ public class LastlegOrderService {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
}
String currentStatus = StringUtils.hasText(query.currentStatus()) ? query.currentStatus() : query.status();
String warehouse = StringUtils.hasText(query.warehouse()) ? query.warehouse().trim() : null;
return new LambdaQueryWrapper<LastlegOrder>()
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
.like(StringUtils.hasText(query.orderNo()), LastlegOrder::getOrderNo, query.orderNo())
@@ -389,11 +525,7 @@ public class LastlegOrderService {
.like(StringUtils.hasText(query.sourceOrderNo()), LastlegOrder::getSourceOrderNo, query.sourceOrderNo())
.like(StringUtils.hasText(query.containerNo()), LastlegOrder::getContainerNo, query.containerNo())
.eq(StringUtils.hasText(currentStatus), LastlegOrder::getCurrentStatus, currentStatus)
.and(StringUtils.hasText(query.warehouse()), wrapper -> wrapper
.like(LastlegOrder::getOverseasWarehouseId, query.warehouse())
.or().like(LastlegOrder::getOverseasWarehouseName, query.warehouse())
.or().like(LastlegOrder::getTransferWarehouseCode, query.warehouse())
.or().like(LastlegOrder::getTransferWarehouseName, query.warehouse()))
.apply(StringUtils.hasText(warehouse), LastlegOrderMapper.WAREHOUSE_FILTER_SQL, warehouse)
.eq(StringUtils.hasText(query.transferWarehouseCode()), LastlegOrder::getTransferWarehouseCode,
query.transferWarehouseCode())
.eq(query.abnormal() != null, LastlegOrder::getAbnormal, query.abnormal())
@@ -433,10 +565,12 @@ public class LastlegOrderService {
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(),
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
o.getInboundCode(), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
displayInboundCode(o), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
o.getOverseasWarehouseCode(), o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(), o.getContainerStatus(),
o.getAbnormal(), o.getDetained(), o.getPalletCount(), o.getPackageCount(), o.getVolumeCbm(),
o.getAbnormal(), o.getDetained(), o.getHoldChargeable(), o.getHoldReason(),
Boolean.TRUE.equals(o.getManualNodeOverride()), Boolean.TRUE.equals(o.getManualNodeOverride()) ? "MANUAL" : "CONTAINER",
o.getVersion() == null ? 0 : o.getVersion(), o.getPalletCount(), o.getPackageCount(), o.getVolumeCbm(),
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), o.getRelabelOrderNo(), o.getRelabelStatus(),
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
@@ -445,6 +579,12 @@ public class LastlegOrderService {
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
}
private static String displayInboundCode(LastlegOrder order) {
String orderType = order.getOrderType();
return "RELABEL_APPLICATION".equals(orderType) || "LOCAL_LOOSEBOARD".equals(orderType)
? null : order.getInboundCode();
}
private static String csv(Object value) {
if (value == null) return "";
String text = String.valueOf(value);
@@ -30,6 +30,9 @@ import com.fasterxml.jackson.databind.JsonNode;
@Service
public class LoadingCompletedConsumer {
static final String DEFAULT_RECEIVING_WAREHOUSE_ID = "193000000000200003";
static final String DEFAULT_RECEIVING_WAREHOUSE_CODE = "US-LAX-01";
static final String DEFAULT_RECEIVING_WAREHOUSE_NAME = "洛杉矶仓";
@Resource private LastlegEventInboxMapper inboxMapper;
@Resource private LastlegOrderMapper orderMapper;
@Resource private LastlegInboundOrderMapper inboundOrderMapper;
@@ -240,7 +243,10 @@ public class LoadingCompletedConsumer {
order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId());
order.setCustomerName(customerName);
order.setSourceWarehouseId(payload.warehouseId()); order.setSourceWarehouseName(payload.warehouseName());
order.setOverseasWarehouseId(null); order.setOverseasWarehouseName(null);
order.setOverseasWarehouseId(DEFAULT_RECEIVING_WAREHOUSE_ID);
order.setOverseasWarehouseCode(DEFAULT_RECEIVING_WAREHOUSE_CODE);
order.setOverseasWarehouseName(DEFAULT_RECEIVING_WAREHOUSE_NAME);
order.setReceivingWarehouseVersion(0);
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(containerNo);
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
order.setPackageCount(source.packageCount());
@@ -335,8 +341,9 @@ public class LoadingCompletedConsumer {
FirstLegLoadingPlanCompletedV1.Item item = source.items().get(index);
if (item.packageCount() <= 0) continue;
inventory.registerInTransit(order.getCustomerId(), "SYSTEM",
new InventoryCommandRequests.InTransit(order.getCustomerName(), payload.warehouseId(), null,
payload.warehouseName(), item.destinationWarehouseId(), item.destinationWarehouseCode(),
new InventoryCommandRequests.InTransit(order.getCustomerName(), order.getOverseasWarehouseId(),
order.getOverseasWarehouseCode(), order.getOverseasWarehouseName(),
item.destinationWarehouseId(), item.destinationWarehouseCode(),
item.destinationWarehouseName(), order.getId(), inboundOrderNo, source.inboundCode(),
item.skuCode(), item.productNameZh(), item.productNameEn(), item.packageCount(),
order.getOrderNo(), event.eventId(),