feat: complete tail order controls and terminal drayage
This commit is contained in:
+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); }
|
||||
}
|
||||
@@ -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,7 @@ 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;
|
||||
@@ -25,6 +27,7 @@ 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;
|
||||
@@ -54,12 +57,16 @@ 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;
|
||||
@@ -243,7 +250,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(),
|
||||
@@ -552,7 +567,9 @@ public class LastlegOrderService {
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user