Files
baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java
T

757 lines
45 KiB
Java
Raw Normal View History

2026-08-19 16:24:02 +08:00
package com.baiyue.lastleg.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
2026-08-26 11:05:26 +08:00
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
2026-08-19 16:24:02 +08:00
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.event.ContainerNumbers;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.dto.LastlegOrderDetailResponse;
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.dto.LastlegOrderRequests;
import com.baiyue.lastleg.dto.LastlegOrderSummary;
2026-08-28 13:54:32 +08:00
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
2026-09-15 16:10:55 +08:00
import com.baiyue.lastleg.dto.ContainerArrivalTimeView;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
2026-08-26 11:05:26 +08:00
import com.baiyue.lastleg.entity.LastlegOrderOperationLog;
import com.baiyue.lastleg.entity.LastlegOrderManualNode;
2026-09-07 15:07:34 +08:00
import com.baiyue.lastleg.entity.ReceivingWarehouseOperation;
2026-09-15 16:10:55 +08:00
import com.baiyue.lastleg.entity.ContainerArrivalTimeOperation;
2026-09-07 15:07:34 +08:00
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
2026-08-28 14:20:48 +08:00
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
2026-09-09 16:37:28 +08:00
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos;
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService;
2026-08-26 11:05:26 +08:00
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
2026-09-07 15:07:34 +08:00
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
2026-09-15 16:10:55 +08:00
import com.baiyue.lastleg.mapper.ContainerArrivalTimeOperationMapper;
2026-08-22 12:35:32 +08:00
import com.baiyue.lastleg.model.LastlegOrderType;
2026-09-18 11:41:04 +08:00
import com.baiyue.lastleg.model.LastlegOrderStatus;
2026-09-11 16:17:16 +08:00
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
2026-08-19 16:24:02 +08:00
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.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
2026-08-31 14:39:40 +08:00
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
2026-08-19 16:24:02 +08:00
import java.time.LocalDateTime;
2026-08-31 15:22:32 +08:00
import java.time.ZoneOffset;
2026-09-07 15:07:34 +08:00
import java.security.MessageDigest;
2026-08-19 16:24:02 +08:00
import java.util.ArrayList;
2026-08-28 16:30:02 +08:00
import java.util.LinkedHashMap;
2026-09-15 16:10:55 +08:00
import java.util.LinkedHashSet;
2026-08-19 16:24:02 +08:00
import java.util.List;
2026-08-28 16:30:02 +08:00
import java.util.Map;
2026-08-19 16:24:02 +08:00
import java.util.Objects;
import java.util.Set;
2026-09-14 09:58:30 +08:00
import java.util.stream.Collectors;
2026-08-19 16:24:02 +08:00
@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";
2026-08-28 14:20:48 +08:00
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");
2026-08-19 16:24:02 +08:00
@Resource private LastlegOrderMapper orderMapper;
@Resource private LastlegOrderDetailMapper detailMapper;
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
2026-09-09 16:37:28 +08:00
@Resource private OverseasFeeSourceService feeSources;
2026-08-26 11:05:26 +08:00
@Resource private LastlegOrderOperationLogMapper operationLogMapper;
@Resource private LastlegOrderManualNodeMapper manualNodeMapper;
@Resource private InboundPlanCartonMapper cartonMapper;
2026-08-28 13:54:32 +08:00
@Resource private FbaWarehouseValidator fbaWarehouseValidator;
2026-08-28 14:20:48 +08:00
@Resource private InventoryCommandService inventory;
2026-08-31 15:22:32 +08:00
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
2026-09-07 15:07:34 +08:00
@Resource private ReceivingWarehouseOperationMapper receivingWarehouseOperations;
2026-09-15 16:10:55 +08:00
@Resource private ContainerArrivalTimeOperationMapper containerArrivalTimeOperations;
2026-09-07 15:07:34 +08:00
@Resource private WarehouseResourceGateway warehouseResourceGateway;
2026-09-11 16:17:16 +08:00
@Resource private ContainerShippingGateMapper containerShippingGates;
2026-08-19 16:24:02 +08:00
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
2026-09-14 09:58:30 +08:00
return page(customerId, query, null);
}
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query, String relabelNo) {
2026-08-19 16:24:02 +08:00
Page<LastlegOrder> page = orderMapper.selectPage(
2026-09-14 09:58:30 +08:00
new Page<>(query.page(), query.size()), filter(customerId, query, relabelNo).orderByDesc(LastlegOrder::getCreatedAt));
2026-08-28 16:30:02 +08:00
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> mergeRelations = palletMergeRelations(page.getRecords());
2026-09-11 16:17:16 +08:00
Map<String, String> containerNodes = containerNodes(page.getRecords());
2026-09-14 09:58:30 +08:00
Map<Long, String> dispatchStatuses = dispatchStatuses(page.getRecords());
return new PageResult<>(page.getRecords().stream().map(order -> summary(order, mergeRelations.get(order.getId()),
2026-09-11 16:17:16 +08:00
StringUtils.hasText(order.getSourceContainerBookingId())
? containerNodes.get(order.getSourceContainerBookingId()) : null, dispatchStatuses.getOrDefault(order.getId(), "UNSCHEDULED"))).toList(),
2026-08-19 16:24:02 +08:00
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
}
2026-08-27 17:16:33 +08:00
@Transactional(readOnly = true)
2026-08-28 13:54:32 +08:00
public PageResult<ContainerPlanProgressSummary> containerProgressPage(
LastlegOrderRequests.ContainerProgressQuery query) {
2026-08-27 17:16:33 +08:00
if (query.createdFrom() != null && query.createdTo() != null
&& query.createdFrom().isAfter(query.createdTo())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"createdFrom must not be after createdTo");
}
2026-08-28 13:54:32 +08:00
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();
2026-08-27 17:16:33 +08:00
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
return new PageResult<>(records, query.page(), query.size(), total, pages);
}
2026-08-28 13:54:32 +08:00
@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));
}
2026-09-15 16:10:55 +08:00
@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 orderMapper.findContainerArrivalTimes(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 = orderMapper.findContainerArrivalTimes(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();
}
2026-09-07 15:07:34 +08:00
@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);
}
2026-09-07 15:07:34 +08:00
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) {
2026-09-07 15:07:34 +08:00
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(),
2026-09-10 16:33:32 +08:00
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;
}
2026-08-28 13:54:32 +08:00
}
2026-09-07 15:07:34 +08:00
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);
}
}
2026-08-19 16:24:02 +08:00
public LastlegOrderDetailResponse detail(String customerId, long orderId) {
LastlegOrder order = findOwned(customerId, orderId);
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
2026-08-24 11:55:50 +08:00
.eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())
2026-08-19 16:24:02 +08:00
.eq(LastlegOrderDetail::getOrderId, orderId).orderByAsc(LastlegOrderDetail::getId));
List<LastlegOrderStatusHistory> history = historyMapper.selectList(
new LambdaQueryWrapper<LastlegOrderStatusHistory>()
2026-08-24 11:55:50 +08:00
.eq(LastlegOrderStatusHistory::getCustomerId, order.getCustomerId())
2026-08-19 16:24:02 +08:00
.eq(LastlegOrderStatusHistory::getOrderId, orderId)
.orderByAsc(LastlegOrderStatusHistory::getCreatedAt));
2026-09-09 16:37:28 +08:00
List<OverseasFeeSource> fees = feeSources.listForBusiness(order.getCustomerId(), "LASTLEG_ORDER", orderId);
2026-08-26 11:05:26 +08:00
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();
2026-09-14 09:58:30 +08:00
return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId()),
2026-09-14 11:02:37 +08:00
containerNode(order), dispatchStatuses(List.of(order)).getOrDefault(order.getId(), "UNSCHEDULED")), manualNodes, cargo, history.stream()
2026-08-20 18:02:38 +08:00
.map(value -> new LastlegOrderDetailResponse.History(value.getId(), value.getFromStatus(),
2026-08-26 11:05:26 +08:00
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());
}
@Transactional
2026-09-09 16:37:28 +08:00
public OverseasFeeSource createFee(String customerId, long orderId, LastlegOrderRequests.CreateFee request,
String operatorName, String idempotencyKey) {
2026-08-26 11:05:26 +08:00
LastlegOrder order = findOwned(customerId, orderId);
LocalDateTime now = LocalDateTime.now();
2026-09-09 16:37:28 +08:00
OverseasFeeSource fee = feeSources.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.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()));
fee = feeSources.submit(fee.getSourceFeeId(), operatorName, childKey(idempotencyKey, "submit"));
2026-08-26 11:05:26 +08:00
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
operation.setId(IdWorker.getId());
operation.setCustomerId(order.getCustomerId());
operation.setOrderId(orderId);
operation.setOperationNode("新增费用");
operation.setOperatorName(operatorName);
2026-09-09 16:37:28 +08:00
operation.setResult("新增" + fee.getFeeItemName() + ":" + fee.getOriginalCurrency() + " "
+ fee.getOriginalAmount().toPlainString() + ",已提交财务审核");
2026-08-26 11:05:26 +08:00
operation.setCreatedAt(now);
operationLogMapper.insert(operation);
return fee;
2026-08-19 16:24:02 +08:00
}
2026-08-28 14:20:48 +08:00
@Transactional
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained request,
String operatorName) {
2026-08-31 15:22:32 +08:00
setDetained(customerId, orderId, request, null, operatorName);
}
@Transactional
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained request,
String operatorId, String operatorName) {
2026-08-28 14:20:48 +08:00
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");
boolean detained = request.detained();
if (Boolean.TRUE.equals(order.getDetained()) == detained) {
throw new ResponseStatusException(HttpStatus.CONFLICT, detained ? "Order is already held" : "Order is not held");
}
if (detained && "INBOUND_COMPLETED".equals(order.getCurrentStatus())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Completed inbound orders cannot be newly held");
}
LocalDateTime now = LocalDateTime.now();
order.setDetained(detained);
order.setUpdatedAt(now);
orderMapper.updateById(order);
if (!detained && "INBOUND_COMPLETED".equals(order.getCurrentStatus())) {
inventory.releaseInboundOrderHold(order.getCustomerId(), operatorName, orderId, order.getOrderNo(), request.remark().trim());
}
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
operation.setId(IdWorker.getId());
operation.setCustomerId(order.getCustomerId());
operation.setOrderId(orderId);
operation.setOperationNode(detained ? "暂扣" : "解除暂扣");
operation.setOperatorName(operatorName);
operation.setResult(request.remark().trim());
operation.setCreatedAt(now);
operationLogMapper.insert(operation);
2026-08-31 15:22:32 +08:00
if (StringUtils.hasText(order.getSourceInboundId())) {
detainedOutbox.publish(order, detained, request.remark().trim(), operatorId, operatorName,
now.toInstant(ZoneOffset.UTC), operation.getId());
}
2026-08-28 14:20:48 +08:00
}
@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);
}
2026-08-31 14:39:40 +08:00
@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);
}
2026-08-19 16:24:02 +08:00
}
@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");
}
2026-08-28 13:54:32 +08:00
FbaWarehouseValidator.VerifiedFbaWarehouse target = fbaWarehouseValidator.required(
request.warehouseId(), request.warehouseCode(), request.warehouseName());
String warehouseId = Long.toString(target.id());
2026-08-19 16:24:02 +08:00
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
2026-08-21 10:43:25 +08:00
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
.in(LastlegOrder::getId, ids).last("FOR UPDATE"));
2026-08-19 16:24:02 +08:00
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();
2026-08-28 13:54:32 +08:00
order.setTransferWarehouseId(warehouseId);
order.setTransferWarehouseCode(target.code());
order.setTransferWarehouseName(target.name());
2026-08-19 16:24:02 +08:00
order.setCurrentStatus(DESTINATION_ASSIGNED);
order.setUpdatedAt(now);
orderMapper.updateById(order);
LastlegOrderDetail detailUpdate = new LastlegOrderDetail();
2026-08-28 13:54:32 +08:00
detailUpdate.setDestinationWarehouseId(warehouseId);
detailUpdate.setDestinationWarehouseCode(target.code());
detailUpdate.setDestinationWarehouseName(target.name());
2026-08-19 16:24:02 +08:00
detailMapper.update(detailUpdate, new LambdaQueryWrapper<LastlegOrderDetail>()
.eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())
2026-08-19 16:24:02 +08:00
.eq(LastlegOrderDetail::getOrderId, order.getId()));
InboundPlanCarton cartonUpdate = new InboundPlanCarton();
2026-08-28 13:54:32 +08:00
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()));
2026-08-19 16:24:02 +08:00
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory();
2026-08-24 11:55:50 +08:00
history.setCustomerId(order.getCustomerId());
2026-08-19 16:24:02 +08:00
history.setOrderId(order.getId());
history.setFromStatus(fromStatus);
history.setToStatus(DESTINATION_ASSIGNED);
history.setRemark(StringUtils.hasText(request.remark())
2026-08-28 13:54:32 +08:00
? request.remark() : "Destination warehouse assigned: " + target.code());
2026-08-19 16:24:02 +08:00
history.setCreatedAt(now);
historyMapper.insert(history);
}
return orders.size();
}
public byte[] exportCsv(String customerId, LastlegOrderRequests.Query query) {
2026-08-31 14:39:40 +08:00
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
writeExportCsv(customerId, query, output);
return output.toByteArray();
} catch (IOException e) {
throw new IllegalStateException("Unable to export tail-leg orders", e);
}
}
public void writeExportCsv(String customerId, LastlegOrderRequests.Query query, OutputStream output)
throws IOException {
2026-09-17 17:37:29 +08:00
output.write("\uFEFFID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜节点,是否异常,是否暂扣,托数,客户预报箱数,实收箱数,差异箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)\r\n".getBytes(StandardCharsets.UTF_8));
2026-08-31 14:39:40 +08:00
final int batchSize = 1000;
long page = 1;
while (true) {
List<LastlegOrder> orders = orderMapper.selectPage(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, batchSize),
filter(customerId, query).orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId)).getRecords();
if (orders.isEmpty()) break;
2026-09-11 16:17:16 +08:00
Map<String, String> containerNodes = containerNodes(orders);
2026-08-31 14:39:40 +08:00
for (LastlegOrder order : orders) {
String line = String.join(",", List.of(
2026-08-22 12:35:32 +08:00
csv(order.getId()), csv(order.getOrderNo()), csv(LastlegOrderType.labelOf(order.getOrderType())),
csv(displayInboundCode(order)),
2026-08-19 16:24:02 +08:00
csv(order.getSourceOrderNo()), csv(order.getCustomerName()), csv(order.getOverseasWarehouseName()),
csv(order.getTransferWarehouseName()), csv(order.getTransferWarehouseCode()), csv(order.getFbaWarehouseCode()),
2026-09-11 16:17:16 +08:00
csv(order.getContainerNo()), csv(StringUtils.hasText(order.getSourceContainerBookingId())
2026-09-18 11:41:04 +08:00
? containerNodes.get(order.getSourceContainerBookingId()) : null), csv(yesOrNo(order.getAbnormal())), csv(yesOrNo(order.getDetained())),
2026-09-17 17:37:29 +08:00
csv(order.getPalletCount()), csv(order.getPackageCount()), csv(order.getReceivedPackageCount()),
csv(packageDifference(order)), csv(order.getVolumeCbm()), csv(order.getProductQuantity()),
2026-08-19 16:24:02 +08:00
csv(order.getVolumeWeightKg()), csv(order.getEstimatedArrivalAt()), csv(order.getActualArrivalAt()),
csv(order.getEstimatedWarehouseAt()), csv(order.getInboundOrderNo()), csv(order.getRelabelOrderNo()), csv(relabelBusinessStatus(order.getRelabelStatus())),
2026-09-18 11:41:04 +08:00
csv(order.getPalletMergeFlag()), csv(LastlegOrderStatus.labelOf(order.getCurrentStatus())), csv(order.getSubOrderStatus()), csv(order.getPrivateRemark()),
2026-08-31 14:39:40 +08:00
csv(order.getCustomerRemark()), csv(order.getWarehouseRemark()))) + "\r\n";
output.write(line.getBytes(StandardCharsets.UTF_8));
}
output.flush();
if (orders.size() < batchSize) break;
page++;
2026-08-19 16:24:02 +08:00
}
}
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query) {
2026-09-14 09:58:30 +08:00
return filter(customerId, query, null);
}
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query, String relabelNo) {
2026-08-19 16:24:02 +08:00
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
}
2026-08-21 14:42:40 +08:00
String currentStatus = StringUtils.hasText(query.currentStatus()) ? query.currentStatus() : query.status();
String warehouse = StringUtils.hasText(query.warehouse()) ? query.warehouse().trim() : null;
2026-08-19 16:24:02 +08:00
return new LambdaQueryWrapper<LastlegOrder>()
2026-08-21 10:43:25 +08:00
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
2026-09-14 15:03:04 +08:00
.and(StringUtils.hasText(query.orderNo()), nested -> nested
.like(LastlegOrder::getOrderNo, query.orderNo())
.or()
.like(LastlegOrder::getSourceOrderNo, query.orderNo()))
2026-09-14 09:58:30 +08:00
.like(StringUtils.hasText(relabelNo), LastlegOrder::getRelabelOrderNo, relabelNo)
2026-08-21 14:42:40 +08:00
.eq(StringUtils.hasText(query.orderType()), LastlegOrder::getOrderType, query.orderType())
2026-08-19 16:24:02 +08:00
.like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode())
2026-09-06 16:02:11 +08:00
.like(StringUtils.hasText(query.lxCode()), LastlegOrder::getLxCode, query.lxCode())
2026-08-19 16:24:02 +08:00
.like(StringUtils.hasText(query.sourceOrderNo()), LastlegOrder::getSourceOrderNo, query.sourceOrderNo())
.like(StringUtils.hasText(query.containerNo()), LastlegOrder::getContainerNo, query.containerNo())
2026-08-21 14:42:40 +08:00
.eq(StringUtils.hasText(currentStatus), LastlegOrder::getCurrentStatus, currentStatus)
.apply(StringUtils.hasText(warehouse), LastlegOrderMapper.WAREHOUSE_FILTER_SQL, warehouse)
2026-08-19 16:24:02 +08:00
.eq(StringUtils.hasText(query.transferWarehouseCode()), LastlegOrder::getTransferWarehouseCode,
query.transferWarehouseCode())
.eq(query.abnormal() != null, LastlegOrder::getAbnormal, query.abnormal())
.eq(query.detained() != null, LastlegOrder::getDetained, query.detained())
.ge(query.createdFrom() != null, LastlegOrder::getCreatedAt, query.createdFrom())
.le(query.createdTo() != null, LastlegOrder::getCreatedAt, query.createdTo());
}
private LastlegOrder findOwned(String customerId, long orderId) {
LastlegOrder order = orderMapper.selectOne(new LambdaQueryWrapper<LastlegOrder>()
2026-08-21 10:43:25 +08:00
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
.eq(LastlegOrder::getId, orderId));
2026-08-19 16:24:02 +08:00
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());
}
}
2026-08-28 16:30:02 +08:00
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;
}
2026-09-14 09:58:30 +08:00
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 LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations,
String containerNode, String dispatchStatus) {
2026-08-28 16:30:02 +08:00
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
2026-09-14 15:03:04 +08:00
return new LastlegOrderSummary(o.getId(), displayOrderNo(o), o.getOrderType(),
2026-08-24 17:15:48 +08:00
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
2026-09-15 16:39:44 +08:00
displayInboundCode(o), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(),
o.getSourceContainerBookingId(), o.getOverseasWarehouseId(),
2026-09-07 15:07:34 +08:00
o.getOverseasWarehouseCode(), o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
2026-09-11 16:17:16 +08:00
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",
2026-09-17 17:37:29 +08:00
o.getVersion() == null ? 0 : o.getVersion(), o.getPalletCount(), o.getPackageCount(),
o.getReceivedPackageCount(), packageDifference(o), o.getVolumeCbm(),
2026-08-19 16:24:02 +08:00
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), dispatchStatus, o.getRelabelOrderNo(), relabelBusinessStatus(o.getRelabelStatus()),
2026-08-28 16:30:02 +08:00
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
2026-08-31 14:39:40 +08:00
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedInboundNo).filter(Objects::nonNull).distinct().toList(),
2026-08-28 16:30:02 +08:00
o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(),
2026-08-20 18:02:38 +08:00
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
2026-08-19 16:24:02 +08:00
}
2026-09-17 17:37:29 +08:00
private static Integer packageDifference(LastlegOrder order) {
if (order.getReceivedPackageCount() == null) return null;
return order.getReceivedPackageCount() - java.util.Objects.requireNonNullElse(order.getPackageCount(), 0);
}
2026-09-14 15:03:04 +08:00
static String displayOrderNo(LastlegOrder order) {
return "WAREHOUSE_TRANSFER".equals(order.getOrderType()) && StringUtils.hasText(order.getSourceOrderNo())
? order.getSourceOrderNo() : order.getOrderNo();
}
2026-09-11 16:17:16 +08:00
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) {
2026-09-15 15:15:46 +08:00
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";
}
2026-09-18 11:41:04 +08:00
private static String yesOrNo(Boolean value) {
if (value == null) return null;
return value ? "是" : "否";
}
2026-09-09 16:37:28 +08:00
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;
}
2026-08-19 16:24:02 +08:00
private static String csv(Object value) {
if (value == null) return "";
String text = String.valueOf(value);
if (!text.isEmpty() && "=+-@".indexOf(text.charAt(0)) >= 0) text = "'" + text;
return '"' + text.replace("\"", "\"\"") + '"';
}
}