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;
|
2026-08-31 12:13:20 +08:00
|
|
|
|
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.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-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.LastlegOrderFee;
|
|
|
|
|
|
import com.baiyue.lastleg.entity.LastlegOrderOperationLog;
|
2026-08-24 15:38:28 +08:00
|
|
|
|
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-08-26 11:05:26 +08:00
|
|
|
|
import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper;
|
|
|
|
|
|
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
2026-08-22 12:35:32 +08:00
|
|
|
|
import com.baiyue.lastleg.model.LastlegOrderType;
|
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-08-19 16:24:02 +08:00
|
|
|
|
import java.util.ArrayList;
|
2026-08-28 16:30:02 +08:00
|
|
|
|
import java.util.LinkedHashMap;
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
@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";
|
2026-08-19 16:24:02 +08:00
|
|
|
|
|
|
|
|
|
|
@Resource private LastlegOrderMapper orderMapper;
|
|
|
|
|
|
@Resource private LastlegOrderDetailMapper detailMapper;
|
|
|
|
|
|
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
|
2026-08-26 11:05:26 +08:00
|
|
|
|
@Resource private LastlegOrderFeeMapper feeMapper;
|
|
|
|
|
|
@Resource private LastlegOrderOperationLogMapper operationLogMapper;
|
2026-08-24 15:38:28 +08:00
|
|
|
|
@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-08-19 16:24:02 +08:00
|
|
|
|
|
|
|
|
|
|
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
|
|
|
|
|
Page<LastlegOrder> page = orderMapper.selectPage(
|
|
|
|
|
|
new Page<>(query.page(), query.size()), filter(customerId, query).orderByDesc(LastlegOrder::getCreatedAt));
|
2026-08-28 16:30:02 +08:00
|
|
|
|
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> mergeRelations = palletMergeRelations(page.getRecords());
|
|
|
|
|
|
return new PageResult<>(page.getRecords().stream().map(order -> summary(order, mergeRelations.get(order.getId()))).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()
|
2026-08-31 12:13:20 +08:00
|
|
|
|
: orderMapper.pageContainerProgress(query, (query.page() - 1) * query.size(), query.size())
|
|
|
|
|
|
.stream().map(this::requireFrozenContainer).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, "来源柜不存在");
|
|
|
|
|
|
}
|
2026-08-31 12:13:20 +08:00
|
|
|
|
return new ContainerPlanProgressDetail(requireFrozenContainer(summary),
|
|
|
|
|
|
orderMapper.findContainerProgressItems(bookingId));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ContainerPlanProgressSummary requireFrozenContainer(ContainerPlanProgressSummary summary) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
ContainerNumbers.requireCanonical(summary.containerNo());
|
|
|
|
|
|
return summary;
|
|
|
|
|
|
} catch (IllegalArgumentException exception) {
|
|
|
|
|
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
|
|
|
|
|
"来源柜真实集装箱号缺失、格式无效或存在漂移", exception);
|
|
|
|
|
|
}
|
2026-08-28 13:54:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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-08-26 11:05:26 +08:00
|
|
|
|
List<LastlegOrderFee> fees = feeMapper.selectList(new LambdaQueryWrapper<LastlegOrderFee>()
|
|
|
|
|
|
.eq(LastlegOrderFee::getCustomerId, order.getCustomerId())
|
|
|
|
|
|
.eq(LastlegOrderFee::getOrderId, orderId).orderByAsc(LastlegOrderFee::getCreatedAt));
|
|
|
|
|
|
List<LastlegOrderOperationLog> operations = operationLogMapper.selectList(
|
|
|
|
|
|
new LambdaQueryWrapper<LastlegOrderOperationLog>()
|
|
|
|
|
|
.eq(LastlegOrderOperationLog::getCustomerId, order.getCustomerId())
|
|
|
|
|
|
.eq(LastlegOrderOperationLog::getOrderId, orderId)
|
|
|
|
|
|
.orderByAsc(LastlegOrderOperationLog::getCreatedAt));
|
2026-08-28 16:30:02 +08:00
|
|
|
|
return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId())), 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
|
|
|
|
|
|
public LastlegOrderFee createFee(String customerId, long orderId, LastlegOrderRequests.CreateFee request,
|
|
|
|
|
|
String operatorName) {
|
|
|
|
|
|
LastlegOrder order = findOwned(customerId, orderId);
|
|
|
|
|
|
LocalDateTime now = LocalDateTime.now();
|
|
|
|
|
|
LastlegOrderFee fee = new LastlegOrderFee();
|
|
|
|
|
|
fee.setId(IdWorker.getId());
|
|
|
|
|
|
fee.setCustomerId(order.getCustomerId());
|
|
|
|
|
|
fee.setOrderId(orderId);
|
|
|
|
|
|
fee.setFeeItem(request.feeItem().trim());
|
|
|
|
|
|
fee.setCurrency(request.currency());
|
|
|
|
|
|
fee.setAmount(request.amount());
|
|
|
|
|
|
fee.setStatus(request.status());
|
|
|
|
|
|
fee.setSupplierDescription(StringUtils.hasText(request.supplierDescription())
|
|
|
|
|
|
? request.supplierDescription().trim() : null);
|
|
|
|
|
|
fee.setCreatedBy(operatorName);
|
|
|
|
|
|
fee.setCreatedAt(now);
|
|
|
|
|
|
fee.setUpdatedAt(now);
|
|
|
|
|
|
feeMapper.insert(fee);
|
|
|
|
|
|
|
|
|
|
|
|
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
|
|
|
|
|
|
operation.setId(IdWorker.getId());
|
|
|
|
|
|
operation.setCustomerId(order.getCustomerId());
|
|
|
|
|
|
operation.setOrderId(orderId);
|
|
|
|
|
|
operation.setOperationNode("新增费用");
|
|
|
|
|
|
operation.setOperatorName(operatorName);
|
|
|
|
|
|
operation.setResult("新增" + fee.getFeeItem() + ":" + fee.getCurrency() + " " + fee.getAmount().toPlainString());
|
|
|
|
|
|
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>()
|
2026-08-24 15:38:28 +08:00
|
|
|
|
.eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())
|
2026-08-19 16:24:02 +08:00
|
|
|
|
.eq(LastlegOrderDetail::getOrderId, order.getId()));
|
|
|
|
|
|
|
2026-08-24 15:38:28 +08:00
|
|
|
|
InboundPlanCarton cartonUpdate = new InboundPlanCarton();
|
2026-08-28 13:54:32 +08:00
|
|
|
|
cartonUpdate.setDestinationWarehouseId(warehouseId);
|
|
|
|
|
|
cartonUpdate.setDestinationWarehouseCode(target.code());
|
|
|
|
|
|
cartonUpdate.setDestinationWarehouseName(target.name());
|
2026-08-24 15:38:28 +08:00
|
|
|
|
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 {
|
|
|
|
|
|
output.write("\uFEFFID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)\r\n".getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
|
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;
|
|
|
|
|
|
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(order.getInboundCode()),
|
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()),
|
|
|
|
|
|
csv(order.getContainerNo()), csv(order.getContainerStatus()), csv(order.getAbnormal()), csv(order.getDetained()),
|
2026-08-20 18:02:38 +08:00
|
|
|
|
csv(order.getPalletCount()), csv(order.getPackageCount()), csv(order.getVolumeCbm()), csv(order.getProductQuantity()),
|
2026-08-19 16:24:02 +08:00
|
|
|
|
csv(order.getVolumeWeightKg()), csv(order.getEstimatedArrivalAt()), csv(order.getActualArrivalAt()),
|
2026-08-20 18:02:38 +08:00
|
|
|
|
csv(order.getEstimatedWarehouseAt()), csv(order.getInboundOrderNo()), csv(order.getRelabelOrderNo()), csv(order.getRelabelStatus()),
|
2026-08-19 16:24:02 +08:00
|
|
|
|
csv(order.getPalletMergeFlag()), csv(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) {
|
|
|
|
|
|
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();
|
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-08-19 16:24:02 +08:00
|
|
|
|
.like(StringUtils.hasText(query.orderNo()), LastlegOrder::getOrderNo, query.orderNo())
|
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)
|
|
|
|
|
|
.and(StringUtils.hasText(query.warehouse()), wrapper -> wrapper
|
|
|
|
|
|
.like(LastlegOrder::getOverseasWarehouseId, query.warehouse())
|
|
|
|
|
|
.or().like(LastlegOrder::getOverseasWarehouseName, query.warehouse())
|
|
|
|
|
|
.or().like(LastlegOrder::getTransferWarehouseCode, query.warehouse())
|
|
|
|
|
|
.or().like(LastlegOrder::getTransferWarehouseName, query.warehouse()))
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations) {
|
|
|
|
|
|
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
|
2026-08-22 12:35:32 +08:00
|
|
|
|
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(),
|
2026-08-24 17:15:48 +08:00
|
|
|
|
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
|
2026-09-06 16:02:11 +08:00
|
|
|
|
o.getInboundCode(), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
|
2026-08-19 16:24:02 +08:00
|
|
|
|
o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
|
|
|
|
|
|
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(), o.getContainerStatus(),
|
2026-08-20 18:02:38 +08:00
|
|
|
|
o.getAbnormal(), o.getDetained(), o.getPalletCount(), o.getPackageCount(), o.getVolumeCbm(),
|
2026-08-19 16:24:02 +08:00
|
|
|
|
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
|
2026-08-20 18:02:38 +08:00
|
|
|
|
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), o.getRelabelOrderNo(), 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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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("\"", "\"\"") + '"';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|