Merge origin/dev and resolve backend conflicts
This commit is contained in:
+3
-1
@@ -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();
|
||||
|
||||
+240
@@ -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); }
|
||||
}
|
||||
+158
-18
@@ -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);
|
||||
|
||||
+10
-3
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user