773 lines
48 KiB
Java
773 lines
48 KiB
Java
package com.baiyue.lastleg.service;
|
||
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||
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;
|
||
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
|
||
import com.baiyue.lastleg.dto.ContainerArrivalTimeView;
|
||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||
import com.baiyue.lastleg.entity.LastlegOrderOperationLog;
|
||
import com.baiyue.lastleg.entity.LastlegOrderManualNode;
|
||
import com.baiyue.lastleg.entity.ReceivingWarehouseOperation;
|
||
import com.baiyue.lastleg.entity.ContainerArrivalTimeOperation;
|
||
import com.baiyue.lastleg.client.OrderPreDepartureClient;
|
||
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;
|
||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos;
|
||
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
|
||
import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService;
|
||
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
|
||
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
|
||
import com.baiyue.lastleg.mapper.ContainerArrivalTimeOperationMapper;
|
||
import com.baiyue.lastleg.model.LastlegOrderType;
|
||
import com.baiyue.lastleg.model.LastlegOrderStatus;
|
||
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
|
||
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
|
||
import jakarta.annotation.Resource;
|
||
import org.springframework.http.HttpStatus;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.transaction.annotation.Transactional;
|
||
import org.springframework.util.StringUtils;
|
||
import org.springframework.beans.factory.annotation.Value;
|
||
import org.springframework.web.server.ResponseStatusException;
|
||
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.io.ByteArrayOutputStream;
|
||
import java.io.IOException;
|
||
import java.io.OutputStream;
|
||
import java.math.BigDecimal;
|
||
import java.time.LocalDateTime;
|
||
import java.security.MessageDigest;
|
||
import java.util.ArrayList;
|
||
import java.util.LinkedHashMap;
|
||
import java.util.LinkedHashSet;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.Objects;
|
||
import java.util.Set;
|
||
import java.util.stream.Collectors;
|
||
|
||
@Service
|
||
public class LastlegOrderService {
|
||
public static final String PENDING_ASSIGNMENT = "PENDING_ASSIGNMENT";
|
||
public static final String DESTINATION_ASSIGNED = "DESTINATION_ASSIGNED";
|
||
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 com.baiyue.lastleg.returnreceipt.service.ReturnReceiptService returnReceipts;
|
||
@Resource private LastlegOrderDetailMapper detailMapper;
|
||
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
|
||
@Resource private OverseasFeeSourceService feeSources;
|
||
@Resource private com.baiyue.lastleg.financefee.service.OverseasManualFeeService manualFees;
|
||
@Resource private LastlegOrderOperationLogMapper operationLogMapper;
|
||
@Resource private LastlegOrderManualNodeMapper manualNodeMapper;
|
||
@Resource private InboundPlanCartonMapper cartonMapper;
|
||
@Resource private FbaWarehouseValidator fbaWarehouseValidator;
|
||
@Resource private InventoryCommandService inventory;
|
||
@Resource private ReceivingWarehouseOperationMapper receivingWarehouseOperations;
|
||
@Resource private ContainerArrivalTimeOperationMapper containerArrivalTimeOperations;
|
||
@Resource private ContainerWarehouseArrivalService warehouseArrival;
|
||
@Resource private WarehouseResourceGateway warehouseResourceGateway;
|
||
@Resource private ContainerShippingGateMapper containerShippingGates;
|
||
@Resource private ReturnOrderReferenceResolver returnOrderReferences;
|
||
@Resource private com.baiyue.lastleg.transfer.service.TransferQuantityService transferQuantities;
|
||
@Resource private OrderPreDepartureClient orderPreDepartureClient;
|
||
@Value("${baiyue.security.internal-service-token:}") private String internalServiceToken;
|
||
|
||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
||
return page(customerId, query, null);
|
||
}
|
||
|
||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query, String relabelNo) {
|
||
if (query.sailingStage() == LastlegOrderRequests.SailingStage.BEFORE_DEPARTURE) {
|
||
Long customer = parseLong(customerId);
|
||
var response = orderPreDepartureClient.page(internalServiceToken, customer, query.orderNo(), query.inboundCode(),
|
||
query.page().intValue(), query.size().intValue());
|
||
if (response == null || response.data() == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "订单服务暂时不可用");
|
||
List<LastlegOrderSummary> rows = response.data().records().stream().map(this::preDepartureSummary).toList();
|
||
return new PageResult<>(rows, response.data().page(), response.data().size(), response.data().total(), response.data().pages());
|
||
}
|
||
if (query.sailingStage() == null && relabelNo == null) return aggregateAllPage(customerId, query);
|
||
Page<LastlegOrder> page = orderMapper.selectPage(
|
||
new Page<>(query.page(), query.size()), filter(customerId, query, relabelNo).orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId));
|
||
return new PageResult<>(nativeSummaries(page.getRecords()),
|
||
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||
}
|
||
|
||
/** 全部及已开船列表使用同一组业务事实,避免聚合列表丢失并托、排车和换标信息。 */
|
||
private List<LastlegOrderSummary> nativeSummaries(List<LastlegOrder> orders) {
|
||
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> mergeRelations = palletMergeRelations(orders);
|
||
returnOrderReferences.populateForRead(orders);
|
||
refreshReturnVolumes(orders);
|
||
Map<String, String> containerNodes = containerNodes(orders);
|
||
Map<Long, String> dispatchStatuses = dispatchStatuses(orders);
|
||
Map<Long, LastlegOrderMapper.OrderWeightRow> weights = orderWeights(orders);
|
||
Map<Long, List<com.baiyue.lastleg.dto.RelabelSubOrder>> relabels = relabelSubOrders(orders);
|
||
Map<Long, LastlegOrderSummary.BusinessSource> businessSources = businessSources(orders);
|
||
var quantities = transferQuantities.forOrders(orders);
|
||
return orders.stream().map(order -> summary(order, mergeRelations.get(order.getId()),
|
||
StringUtils.hasText(order.getSourceContainerBookingId())
|
||
? containerNodes.get(order.getSourceContainerBookingId()) : null,
|
||
dispatchStatuses.getOrDefault(order.getId(), "UNSCHEDULED"), weights.get(order.getId()),
|
||
relabels.getOrDefault(order.getId(), List.of()), businessSources.get(order.getId()), quantities.get(order.getId()))).toList();
|
||
}
|
||
|
||
/** 全部页签合并订单服务的未开船头程数据与本服务已有尾程单,尾程记录优先覆盖同一来源订单。 */
|
||
private PageResult<LastlegOrderSummary> aggregateAllPage(String customerId, LastlegOrderRequests.Query query) {
|
||
var source = orderPreDepartureClient.page(internalServiceToken, parseLong(customerId), query.orderNo(), query.inboundCode(), 1, 2000);
|
||
if (source == null || source.data() == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "订单服务暂时不可用");
|
||
List<LastlegOrder> existing = orderMapper.selectList(filter(customerId, query, null)
|
||
.orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId));
|
||
Map<String, LastlegOrderSummary> merged = new LinkedHashMap<>();
|
||
source.data().records().forEach(row -> {
|
||
var summary = preDepartureSummary(row);
|
||
merged.put(orderKey(summary.customerId(), summary.sourceOrderNo()), summary);
|
||
});
|
||
for (LastlegOrderSummary order : nativeSummaries(existing)) {
|
||
String key = orderKey(order.customerId(), StringUtils.hasText(order.sourceOrderNo()) ? order.sourceOrderNo() : order.orderNo());
|
||
merged.put(key, order);
|
||
}
|
||
List<LastlegOrderSummary> rows = merged.values().stream().sorted(java.util.Comparator.comparing(LastlegOrderSummary::createdAt,
|
||
java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder()))).toList();
|
||
int from = Math.min((query.page().intValue() - 1) * query.size().intValue(), rows.size());
|
||
int to = Math.min(from + query.size().intValue(), rows.size());
|
||
long total = rows.size();
|
||
return new PageResult<>(rows.subList(from, to), query.page(), query.size(), total,
|
||
total == 0 ? 0 : (total + query.size() - 1) / query.size());
|
||
}
|
||
|
||
private static String orderKey(String customerId, String orderNo) {
|
||
return String.valueOf(customerId) + ":" + String.valueOf(orderNo);
|
||
}
|
||
|
||
/** 将订单服务权威头程摘要投影为只读尾程列表行;尚未生成尾程单的字段保持为空。 */
|
||
private LastlegOrderSummary preDepartureSummary(OrderPreDepartureClient.Row row) {
|
||
LastlegOrder order = new LastlegOrder();
|
||
// 头程聚合行尚未生成尾程单,尾程订单号必须保持为空;来源头程订单号单独保留。
|
||
order.setId(row.orderId()); order.setOrderNo(null); order.setOrderType("FIRST_LEG");
|
||
order.setInboundCode(row.inboundCode()); order.setLxCode(row.lxCode()); order.setSourceOrderNo(row.orderNo());
|
||
order.setCustomerId(row.customerId() == null ? null : String.valueOf(row.customerId())); order.setCustomerName(row.customerName());
|
||
order.setSourceWarehouseName(row.receivingWarehouseName()); order.setOverseasWarehouseName(row.receivingWarehouseName());
|
||
order.setFbaWarehouseCode(row.destinationCode());
|
||
order.setContainerNo(row.containerNo()); order.setPackageCount(row.packageCount()); order.setVolumeCbm(row.volumeCbm());
|
||
order.setEstimatedArrivalAt(row.expectedArrivalAt()); order.setCurrentStatus(row.mainStatus());
|
||
order.setCreatedAt(row.createdAt()); order.setUpdatedAt(row.updatedAt()); order.setVersion(0);
|
||
return summary(order, List.of(), null, "UNSCHEDULED", null, List.of(), null, null);
|
||
}
|
||
|
||
@Transactional(readOnly = true)
|
||
public PageResult<ContainerPlanProgressSummary> containerProgressPage(
|
||
LastlegOrderRequests.ContainerProgressQuery query) {
|
||
if (query.createdFrom() != null && query.createdTo() != null
|
||
&& query.createdFrom().isAfter(query.createdTo())) {
|
||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||
"createdFrom must not be after createdTo");
|
||
}
|
||
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::containerSummaryForRead).toList();
|
||
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
|
||
return new PageResult<>(records, query.page(), query.size(), total, pages);
|
||
}
|
||
|
||
@Transactional(readOnly = true)
|
||
public ContainerPlanProgressDetail containerProgressDetail(String sourceContainerBookingId) {
|
||
String bookingId = sourceContainerBookingId == null ? null : sourceContainerBookingId.trim();
|
||
if (!StringUtils.hasText(bookingId)) {
|
||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订ID不能为空");
|
||
}
|
||
ContainerPlanProgressSummary summary = orderMapper.findContainerProgress(bookingId);
|
||
if (summary == null) {
|
||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
|
||
}
|
||
return new ContainerPlanProgressDetail(containerSummaryForRead(summary),
|
||
orderMapper.findContainerProgressItems(bookingId));
|
||
}
|
||
|
||
/** 清关列表读取统一解析的柜级日期,默认推算不写入订单。 */
|
||
@Transactional(readOnly = true)
|
||
public List<ContainerArrivalTimeView> containerArrivalTimes(
|
||
LastlegOrderRequests.ContainerArrivalTimeQuery request) {
|
||
LinkedHashSet<String> bookingIds = new LinkedHashSet<>();
|
||
for (String value : request.sourceContainerBookingIds()) bookingIds.add(normalizeBookingId(value));
|
||
if (bookingIds.size() != request.sourceContainerBookingIds().size()) {
|
||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订ID不能重复");
|
||
}
|
||
return warehouseArrival.resolve(List.copyOf(bookingIds));
|
||
}
|
||
|
||
@Transactional
|
||
public ContainerArrivalTimeView changeExpectedWarehouseArrival(String sourceContainerBookingId,
|
||
String idempotencyKey, LastlegOrderRequests.ChangeExpectedWarehouseArrival request,
|
||
String operatorName) {
|
||
String bookingId = normalizeBookingId(sourceContainerBookingId);
|
||
String key = normalizeIdempotencyKey(idempotencyKey);
|
||
String requestHash = hash(bookingId + "\n" + request.estimatedWarehouseArrivalAt() + "\n"
|
||
+ request.expectedVersion() + "\n" + Objects.toString(request.remark(), ""));
|
||
ContainerArrivalTimeOperation replay = containerArrivalTimeOperations.findByKey(key);
|
||
if (replay != null) {
|
||
if (!bookingId.equals(replay.getSourceContainerBookingId())
|
||
|| !requestHash.equals(replay.getRequestHash())) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||
"Idempotency-Key 已用于其他预计到仓时间修改请求");
|
||
}
|
||
return requireContainerArrivalTime(bookingId);
|
||
}
|
||
|
||
List<LastlegOrder> orders = orderMapper.lockContainerOrders(bookingId);
|
||
if (orders.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
|
||
int version = requireSingleArrivalTimeVersion(orders);
|
||
if (version != request.expectedVersion()) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT, "预计到仓时间版本已变化,请刷新后重试");
|
||
}
|
||
if (orderMapper.countContainerPlans(bookingId) > 0) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT, "该柜已生成入库计划,预计到仓时间不可修改");
|
||
}
|
||
int updated = orderMapper.updateContainerExpectedWarehouseArrival(bookingId,
|
||
request.estimatedWarehouseArrivalAt(), version);
|
||
if (updated != orders.size()) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内预计到仓时间发生并发变化");
|
||
}
|
||
ContainerArrivalTimeOperation operation = new ContainerArrivalTimeOperation();
|
||
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()); containerArrivalTimeOperations.insert(operation);
|
||
return requireContainerArrivalTime(bookingId);
|
||
}
|
||
|
||
/** 保存后沿同一读取入口返回人工日期、版本和计划冻结事实。 */
|
||
private ContainerArrivalTimeView requireContainerArrivalTime(String bookingId) {
|
||
List<ContainerArrivalTimeView> values = warehouseArrival.resolve(List.of(bookingId));
|
||
if (values.size() != 1) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
|
||
if (values.get(0).version() < 0) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内预计到仓时间版本不一致");
|
||
}
|
||
return values.get(0);
|
||
}
|
||
|
||
private int requireSingleArrivalTimeVersion(List<LastlegOrder> orders) {
|
||
Set<Integer> versions = orders.stream().map(LastlegOrder::getArrivalTimeVersion)
|
||
.map(value -> value == null ? 0 : value).collect(Collectors.toSet());
|
||
if (versions.size() != 1) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内预计到仓时间版本不一致");
|
||
}
|
||
return versions.iterator().next();
|
||
}
|
||
|
||
@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,
|
||
"海外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.resourcePendingPlanCount(), 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);
|
||
}
|
||
}
|
||
|
||
/** 返回订单详情及统一费用审核、余额锁扣状态。 */
|
||
public LastlegOrderDetailResponse detail(String customerId, long orderId) {
|
||
LastlegOrder order = findOwned(customerId, orderId);
|
||
returnOrderReferences.populateForRead(List.of(order));
|
||
refreshReturnVolumes(List.of(order));
|
||
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||
.eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())
|
||
.eq(LastlegOrderDetail::getOrderId, orderId).orderByAsc(LastlegOrderDetail::getId));
|
||
List<LastlegOrderStatusHistory> history = historyMapper.selectList(
|
||
new LambdaQueryWrapper<LastlegOrderStatusHistory>()
|
||
.eq(LastlegOrderStatusHistory::getCustomerId, order.getCustomerId())
|
||
.eq(LastlegOrderStatusHistory::getOrderId, orderId)
|
||
.orderByAsc(LastlegOrderStatusHistory::getCreatedAt));
|
||
List<OverseasFeeSource> orderFees = new java.util.ArrayList<>(
|
||
feeSources.listForBusiness(order.getCustomerId(), "LASTLEG_ORDER", orderId));
|
||
// 换标费用以申请为主业务,履约详情同时保留旧订单费用和新申请费用。
|
||
if ("RELABEL_APPLICATION".equals(order.getOrderType()))
|
||
orderFees.addAll(manualFees.relabelFeesForOrder(order.getCustomerId(), orderId));
|
||
List<OverseasFeeSource> fees = manualFees.describe(orderFees);
|
||
List<LastlegOrderOperationLog> operations = operationLogMapper.selectList(
|
||
new LambdaQueryWrapper<LastlegOrderOperationLog>()
|
||
.eq(LastlegOrderOperationLog::getCustomerId, order.getCustomerId())
|
||
.eq(LastlegOrderOperationLog::getOrderId, orderId)
|
||
.orderByAsc(LastlegOrderOperationLog::getCreatedAt));
|
||
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()),
|
||
containerNode(order), dispatchStatuses(List.of(order)).getOrDefault(order.getId(), "UNSCHEDULED"),
|
||
orderWeights(List.of(order)).get(order.getId()), relabelSubOrders(List.of(order)).getOrDefault(order.getId(), List.of()),
|
||
businessSources(List.of(order)).get(order.getId()), transferQuantities.forOrders(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(),
|
||
value.getOperatorName(), value.getResult(), value.getCreatedAt())).toList(),
|
||
"RETURN_RELABEL".equals(order.getOrderType()) && StringUtils.hasText(order.getSourceOrderNo())
|
||
? returnReceipts.detail(order.getCustomerId(), order.getSourceOrderNo()) : null);
|
||
}
|
||
|
||
/** 订单详情统一委托人工费用服务,头程待审核入账,海外业务先锁款。 */
|
||
@Transactional
|
||
public OverseasFeeSource createFee(String customerId, long orderId, LastlegOrderRequests.CreateFee request,
|
||
String operatorName, String idempotencyKey) {
|
||
LastlegOrder order = findOwned(customerId, orderId);
|
||
LocalDateTime now = LocalDateTime.now();
|
||
OverseasFeeSource fee = manualFees.create(operatorName, idempotencyKey,
|
||
new OverseasFeeSourceDtos.SaveRequest(order.getCustomerId(), order.getCustomerName(),
|
||
"OVERSEAS_ORDER_FEE", "LASTLEG_ORDER", orderId, order.getOrderNo(), orderId,
|
||
null, order.getInboundCode(), null, parseLong(order.getSourceContainerBookingId()),
|
||
order.getContainerNo(), parseLong(order.getOverseasWarehouseId()), request.feeItemCode(),
|
||
request.feeItemName(), request.feeRemark(), request.direction(), request.supplierId(), request.supplierName(),
|
||
request.supplierCategory(), request.settlementMode(), request.paidAt(), request.paymentMethod(),
|
||
request.paymentReferenceNo(), request.paymentVoucherFileIds(), request.businessDate(),
|
||
request.currency(), request.amount(), request.chargeableQuantity(), request.quantityUnit(),
|
||
request.unitPrice(), request.periodStart(), request.periodEnd()));
|
||
|
||
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
|
||
operation.setId(IdWorker.getId());
|
||
operation.setCustomerId(order.getCustomerId());
|
||
operation.setOrderId(orderId);
|
||
operation.setOperationNode("新增费用");
|
||
operation.setOperatorName(operatorName);
|
||
operation.setResult("新增" + fee.getFeeItemName() + ":" + fee.getOriginalCurrency() + " "
|
||
+ fee.getOriginalAmount().toPlainString() + ",已提交财务审核");
|
||
operation.setCreatedAt(now);
|
||
operationLogMapper.insert(operation);
|
||
return fee;
|
||
}
|
||
|
||
@Transactional
|
||
public void setPalletMergeRequired(String customerId, long orderId,
|
||
LastlegOrderRequests.SetPalletMergeRequired request,
|
||
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");
|
||
if ("INBOUND_COMPLETED".equals(order.getCurrentStatus())) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||
"Completed inbound orders cannot change the pallet merge mark");
|
||
}
|
||
if (PALLET_MERGED.equals(order.getPalletMergeFlag())) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||
"A completed pallet merge cannot be unmarked");
|
||
}
|
||
String target = request.palletMergeRequired() ? PALLET_MERGE_REQUIRED : null;
|
||
if (Objects.equals(target, order.getPalletMergeFlag())) return;
|
||
LocalDateTime now = LocalDateTime.now();
|
||
if (orderMapper.updatePalletMergeFlag(order.getCustomerId(), orderId, target, now) != 1) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Pallet merge mark was not updated");
|
||
}
|
||
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
|
||
operation.setId(IdWorker.getId());
|
||
operation.setCustomerId(order.getCustomerId());
|
||
operation.setOrderId(orderId);
|
||
operation.setOperationNode(request.palletMergeRequired() ? "标记合托" : "取消合托标记");
|
||
operation.setOperatorName(operatorName);
|
||
operation.setResult(request.palletMergeRequired()
|
||
? "PDA扫描本单箱唛时提示需要合托" : "已取消PDA合托提示");
|
||
operation.setCreatedAt(now);
|
||
operationLogMapper.insert(operation);
|
||
}
|
||
|
||
@Transactional
|
||
public void setPalletMergeBatch(String customerId, LastlegOrderRequests.SetPalletMergeBatch request,
|
||
String operatorName) {
|
||
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
|
||
if (ids.isEmpty()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
|
||
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||
.in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||
if (orders.size() != ids.size()) {
|
||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more tail-leg orders were not found");
|
||
}
|
||
if (orders.stream().map(LastlegOrder::getCustomerId).distinct().count() != 1) {
|
||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Pallet merge orders must belong to one customer");
|
||
}
|
||
if (orders.stream().map(order -> StringUtils.hasText(order.getTransferWarehouseCode())
|
||
? order.getTransferWarehouseCode() : order.getFbaWarehouseCode()).filter(Objects::nonNull).distinct().count() > 1) {
|
||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Pallet merge orders must have one destination warehouse");
|
||
}
|
||
if (orders.stream().anyMatch(order -> "INBOUND_COMPLETED".equals(order.getCurrentStatus())
|
||
|| PALLET_MERGED.equals(order.getPalletMergeFlag()))) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Completed pallet merge orders cannot be changed");
|
||
}
|
||
String batchId = request.palletMergeRequired()
|
||
? "PMG-" + java.util.UUID.randomUUID().toString().replace("-", "") : null;
|
||
String flag = request.palletMergeRequired() ? PALLET_MERGE_REQUIRED : null;
|
||
LocalDateTime now = LocalDateTime.now();
|
||
for (LastlegOrder order : orders) {
|
||
if (orderMapper.updatePalletMergeBatch(order.getCustomerId(), order.getId(), flag, batchId, now) != 1) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Pallet merge group was not updated");
|
||
}
|
||
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
|
||
operation.setId(IdWorker.getId()); operation.setCustomerId(order.getCustomerId());
|
||
operation.setOrderId(order.getId());
|
||
operation.setOperationNode(request.palletMergeRequired() ? "批量标记合托" : "取消合托标记");
|
||
operation.setOperatorName(operatorName);
|
||
operation.setResult(request.palletMergeRequired() ? "PDA合托分组 " + batchId : "已取消PDA合托分组");
|
||
operation.setCreatedAt(now); operationLogMapper.insert(operation);
|
||
}
|
||
}
|
||
|
||
@Transactional
|
||
public int transferDestination(String customerId, LastlegOrderRequests.TransferDestination request) {
|
||
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
|
||
if (ids.isEmpty()) {
|
||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
|
||
}
|
||
FbaWarehouseValidator.VerifiedFbaWarehouse target = fbaWarehouseValidator.required(
|
||
request.warehouseId(), request.warehouseCode(), request.warehouseName());
|
||
String warehouseId = Long.toString(target.id());
|
||
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||
.in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||
if (orders.size() != ids.size()) {
|
||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more tail-leg orders were not found");
|
||
}
|
||
for (LastlegOrder order : orders) {
|
||
ensureTransferable(order);
|
||
}
|
||
LocalDateTime now = LocalDateTime.now();
|
||
for (LastlegOrder order : orders) {
|
||
String fromStatus = order.getCurrentStatus();
|
||
order.setTransferWarehouseId(warehouseId);
|
||
order.setTransferWarehouseCode(target.code());
|
||
order.setTransferWarehouseName(target.name());
|
||
order.setCurrentStatus(DESTINATION_ASSIGNED);
|
||
order.setUpdatedAt(now);
|
||
orderMapper.updateById(order);
|
||
|
||
LastlegOrderDetail detailUpdate = new LastlegOrderDetail();
|
||
detailUpdate.setDestinationWarehouseId(warehouseId);
|
||
detailUpdate.setDestinationWarehouseCode(target.code());
|
||
detailUpdate.setDestinationWarehouseName(target.name());
|
||
detailMapper.update(detailUpdate, new LambdaQueryWrapper<LastlegOrderDetail>()
|
||
.eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())
|
||
.eq(LastlegOrderDetail::getOrderId, order.getId()));
|
||
|
||
InboundPlanCarton cartonUpdate = new InboundPlanCarton();
|
||
cartonUpdate.setDestinationWarehouseId(warehouseId);
|
||
cartonUpdate.setDestinationWarehouseCode(target.code());
|
||
cartonUpdate.setDestinationWarehouseName(target.name());
|
||
cartonMapper.update(cartonUpdate, new LambdaQueryWrapper<InboundPlanCarton>()
|
||
.eq(InboundPlanCarton::getCustomerId, order.getCustomerId())
|
||
.eq(InboundPlanCarton::getOrderId, order.getId()));
|
||
|
||
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory();
|
||
history.setCustomerId(order.getCustomerId());
|
||
history.setOrderId(order.getId());
|
||
history.setFromStatus(fromStatus);
|
||
history.setToStatus(DESTINATION_ASSIGNED);
|
||
history.setRemark(StringUtils.hasText(request.remark())
|
||
? request.remark() : "Destination warehouse assigned: " + target.code());
|
||
history.setCreatedAt(now);
|
||
historyMapper.insert(history);
|
||
}
|
||
return orders.size();
|
||
}
|
||
|
||
/** 与主列表复用同一筛选,汇总只纳入仓储转运且不使用分页参数。 */
|
||
public com.baiyue.lastleg.transfer.dto.TransferQuantityTotals transferQuantityTotals(
|
||
String customerId, LastlegOrderRequests.Query query, String relabelNo) {
|
||
return transferQuantities.totals(filter(customerId, query, relabelNo).eq(LastlegOrder::getOrderType,"WAREHOUSE_TRANSFER"));
|
||
}
|
||
|
||
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query) {
|
||
return filter(customerId, query, null);
|
||
}
|
||
|
||
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query, String relabelNo) {
|
||
return com.baiyue.lastleg.query.LastlegOrderFilter.create(customerId, query, relabelNo);
|
||
}
|
||
|
||
private LastlegOrder findOwned(String customerId, long orderId) {
|
||
LastlegOrder order = orderMapper.selectOne(new LambdaQueryWrapper<LastlegOrder>()
|
||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||
.eq(LastlegOrder::getId, orderId));
|
||
if (order == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tail-leg order not found");
|
||
return order;
|
||
}
|
||
|
||
private void ensureTransferable(LastlegOrder order) {
|
||
if (StringUtils.hasText(order.getInboundOrderNo()) || INBOUND_PLANNED.equals(order.getCurrentStatus())) {
|
||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||
"Destination warehouse cannot be changed after inbound plan creation: " + order.getId());
|
||
}
|
||
}
|
||
|
||
private Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> palletMergeRelations(List<LastlegOrder> orders) {
|
||
if (orders.isEmpty()) return Map.of();
|
||
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> result = new LinkedHashMap<>();
|
||
List<LastlegOrderMapper.PalletMergeRelationRow> rows = orderMapper.findPalletMergeRelations(
|
||
orders.stream().map(LastlegOrder::getId).toList());
|
||
if (rows == null) return Map.of();
|
||
for (LastlegOrderMapper.PalletMergeRelationRow row : rows) {
|
||
result.computeIfAbsent(row.orderId(), ignored -> new ArrayList<>()).add(row);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private Map<Long, String> dispatchStatuses(List<LastlegOrder> orders) {
|
||
if (orders.isEmpty()) return Map.of();
|
||
List<LastlegOrderMapper.DispatchStatusRow> rows = orderMapper.findDispatchStatuses(
|
||
orders.stream().map(LastlegOrder::getId).toList());
|
||
if (rows == null) return Map.of();
|
||
return rows.stream().collect(Collectors.toMap(LastlegOrderMapper.DispatchStatusRow::orderId,
|
||
LastlegOrderMapper.DispatchStatusRow::dispatchStatus));
|
||
}
|
||
|
||
private void refreshReturnVolumes(List<LastlegOrder> orders) {
|
||
List<Long> ids = orders.stream().filter(order -> "RETURN_RELABEL".equals(order.getOrderType())).map(LastlegOrder::getId).toList();
|
||
if (ids.isEmpty()) return;
|
||
Map<Long, BigDecimal> volumes = orderMapper.findReturnVolumes(ids).stream()
|
||
.collect(Collectors.toMap(LastlegOrderMapper.ReturnVolumeRow::orderId, LastlegOrderMapper.ReturnVolumeRow::volumeCbm));
|
||
for (LastlegOrder order : orders) if (volumes.containsKey(order.getId())) order.setVolumeCbm(volumes.get(order.getId()));
|
||
}
|
||
|
||
private Map<Long, LastlegOrderMapper.OrderWeightRow> orderWeights(List<LastlegOrder> orders) {
|
||
if (orders.isEmpty()) return Map.of();
|
||
List<LastlegOrderMapper.OrderWeightRow> rows = orderMapper.findOrderWeights(
|
||
orders.stream().map(LastlegOrder::getId).toList());
|
||
if (rows == null) return Map.of();
|
||
return rows.stream().collect(Collectors.toMap(LastlegOrderMapper.OrderWeightRow::orderId, row -> row));
|
||
}
|
||
|
||
private Map<Long, List<com.baiyue.lastleg.dto.RelabelSubOrder>> relabelSubOrders(List<LastlegOrder> orders) {
|
||
if (orders.isEmpty()) return Map.of();
|
||
var rows = orderMapper.findRelabelSubOrders(orders.stream().map(LastlegOrder::getId).toList());
|
||
return rows == null ? Map.of() : rows.stream().collect(Collectors.groupingBy(com.baiyue.lastleg.dto.RelabelSubOrder::lastlegOrderId));
|
||
}
|
||
|
||
private Map<Long, LastlegOrderSummary.BusinessSource> businessSources(List<LastlegOrder> orders) {
|
||
if (orders.isEmpty()) return Map.of();
|
||
return orderMapper.findBusinessSources(orders.stream().map(LastlegOrder::getId).toList()).stream()
|
||
.collect(Collectors.toMap(LastlegOrderMapper.BusinessSourceRow::orderId, row -> {
|
||
String label = "US_TRUCK".equals(row.businessType()) ? "本土卡车" : LastlegOrderType.labelOf(row.businessType());
|
||
if (row.poolSource()) label += " · 拼车需求池";
|
||
else if ("LOCAL_LOOSEBOARD".equals(row.businessType())) label += " · 独立下单";
|
||
else if (StringUtils.hasText(row.externalBookingNo())) label += " · 外接来货";
|
||
return new LastlegOrderSummary.BusinessSource(row.businessType(), label, row.businessOrderNo(), row.externalBookingNo());
|
||
}));
|
||
}
|
||
|
||
private LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations,
|
||
String containerNode, String dispatchStatus,
|
||
LastlegOrderMapper.OrderWeightRow weight,
|
||
List<com.baiyue.lastleg.dto.RelabelSubOrder> relabelApplications,
|
||
LastlegOrderSummary.BusinessSource businessSource,
|
||
com.baiyue.lastleg.transfer.dto.TransferQuantityView transferQuantity) {
|
||
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
|
||
return new LastlegOrderSummary(o.getId(), displayOrderNo(o), o.getOrderType(),
|
||
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
|
||
displayInboundCode(o), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(),
|
||
o.getSourceContainerBookingId(), o.getOverseasWarehouseId(),
|
||
o.getOverseasWarehouseCode(), o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
|
||
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(), containerNode,
|
||
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.getReceivedPackageCount(), packageDifference(o), weight == null ? BigDecimal.ZERO : weight.grossWeightKg(),
|
||
weight == null ? BigDecimal.ZERO : weight.netWeightKg(), o.getVolumeCbm(),
|
||
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
|
||
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), dispatchStatus, o.getRelabelOrderNo(), relabelBusinessStatus(o.getRelabelStatus()),
|
||
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
|
||
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedInboundNo).filter(Objects::nonNull).distinct().toList(),
|
||
o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(),
|
||
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt(), relabelApplications, businessSource, transferQuantity);
|
||
}
|
||
|
||
private static Integer packageDifference(LastlegOrder order) {
|
||
if (order.getReceivedPackageCount() == null) return null;
|
||
return order.getReceivedPackageCount() - java.util.Objects.requireNonNullElse(order.getPackageCount(), 0);
|
||
}
|
||
|
||
static String displayOrderNo(LastlegOrder order) {
|
||
return ("WAREHOUSE_TRANSFER".equals(order.getOrderType()) || "RETURN_RELABEL".equals(order.getOrderType())) && StringUtils.hasText(order.getSourceOrderNo())
|
||
? order.getSourceOrderNo() : order.getOrderNo();
|
||
}
|
||
|
||
private String containerNode(LastlegOrder order) {
|
||
if (!StringUtils.hasText(order.getSourceContainerBookingId())) return null;
|
||
ContainerShippingGate gate = containerShippingGates.selectById(order.getSourceContainerBookingId());
|
||
return gate == null ? null : gate.getStandardNode();
|
||
}
|
||
|
||
private Map<String, String> containerNodes(List<LastlegOrder> orders) {
|
||
List<String> bookingIds = orders.stream().map(LastlegOrder::getSourceContainerBookingId)
|
||
.filter(StringUtils::hasText).distinct().toList();
|
||
if (bookingIds.isEmpty()) return Map.of();
|
||
Map<String, String> result = new LinkedHashMap<>();
|
||
containerShippingGates.selectBatchIds(bookingIds).forEach(gate -> {
|
||
if (StringUtils.hasText(gate.getStandardNode())) {
|
||
result.put(gate.getContainerBookingId(), gate.getStandardNode());
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
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 relabelBusinessStatus(String workflowStatus) {
|
||
if ("PROCESSING".equals(workflowStatus)) return "PROCESSING";
|
||
if (!StringUtils.hasText(workflowStatus) || "NOT_REQUIRED".equals(workflowStatus)) return workflowStatus;
|
||
return Set.of("READY_FOR_OUTBOUND", "COMPLETED").contains(workflowStatus)
|
||
? "COMPLETED" : "PENDING_PROCESSING";
|
||
}
|
||
|
||
private static Long parseLong(String value) {
|
||
try { return StringUtils.hasText(value) ? Long.parseLong(value.trim()) : null; }
|
||
catch (NumberFormatException ignored) { return null; }
|
||
}
|
||
|
||
private static String childKey(String key, String suffix) {
|
||
String prefix = key == null ? "" : key.trim();
|
||
int maxPrefix = Math.max(0, 128 - suffix.length() - 1);
|
||
return prefix.substring(0, Math.min(prefix.length(), maxPrefix)) + ':' + suffix;
|
||
}
|
||
|
||
}
|
||
|