暂扣 标记合托功能

This commit is contained in:
2026-08-28 14:20:48 +08:00
parent 6a83008ba6
commit 66b9c3aa77
34 changed files with 416 additions and 51 deletions
@@ -93,6 +93,31 @@ public class LastlegOrderController {
return ApiResponse.success(orderService.createFee(platformScope(user, customerId), id, request, operatorName(user))); return ApiResponse.success(orderService.createFee(platformScope(user, customerId), id, request, operatorName(user)));
} }
@PutMapping("/api/platform/v1/last-leg/orders/{id}/detained")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
@Operation(summary = "Set tail-leg order hold status", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
public ApiResponse<Void> setDetained(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable long id,
@Valid @RequestBody LastlegOrderRequests.SetDetained request) {
orderService.setDetained(platformScope(user, customerId), id, request, operatorName(user));
return ApiResponse.success(null);
}
@PutMapping("/api/platform/v1/last-leg/orders/{id}/pallet-merge-required")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
@Operation(summary = "Set the PDA pallet-merge reminder for one tail-leg order",
description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
public ApiResponse<Void> setPalletMergeRequired(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable long id,
@Valid @RequestBody LastlegOrderRequests.SetPalletMergeRequired request) {
orderService.setPalletMergeRequired(platformScope(user, customerId), id, request, operatorName(user));
return ApiResponse.success(null);
}
@GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv") @GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EXPORT + "')") @PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EXPORT + "')")
@Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EXPORT) @Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EXPORT)
@@ -53,6 +53,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
@@ -77,6 +78,7 @@ public class DeliveryTransferService {
@Resource private LastlegOrderMapper tailOrders; @Resource private LastlegOrderMapper tailOrders;
@Resource private LastlegOrderStatusHistoryMapper histories; @Resource private LastlegOrderStatusHistoryMapper histories;
@Resource private LastlegOrderDetailMapper tailOrderDetails; @Resource private LastlegOrderDetailMapper tailOrderDetails;
@Resource private LastlegOrderNoGenerator orderNoGenerator;
@Transactional @Transactional
public DeliveryResponses.TransferResult transfer(String customerId, String dispatchNo, String operatorId, public DeliveryResponses.TransferResult transfer(String customerId, String dispatchNo, String operatorId,
@@ -105,7 +107,7 @@ public class DeliveryTransferService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
OutboundOrder outbound = new OutboundOrder(); OutboundOrder outbound = new OutboundOrder();
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId); outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
outbound.setOutboundNo("OUT" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + outbound.getId()); outbound.setOutboundNo(orderNoGenerator.next("OUTBOUND", dispatch.getOverseasWarehouseId(), outbound.getId()));
outbound.setDispatchOrderId(dispatch.getId()); outbound.setDispatchNo(dispatch.getDispatchNo()); outbound.setDispatchOrderId(dispatch.getId()); outbound.setDispatchNo(dispatch.getDispatchNo());
outbound.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); outbound.setStatus("WAIT_OUTBOUND"); outbound.setOutboundType("DISPATCH_DELIVERY"); outbound.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); outbound.setStatus("WAIT_OUTBOUND"); outbound.setOutboundType("DISPATCH_DELIVERY");
outbound.setPackageCount(items.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum()); outbound.setPackageCount(items.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
@@ -141,7 +143,7 @@ public class DeliveryTransferService {
DeliveryOrder delivery = new DeliveryOrder(); DeliveryOrder delivery = new DeliveryOrder();
delivery.setId(IdWorker.getId()); delivery.setCustomerId(customerId); delivery.setId(IdWorker.getId()); delivery.setCustomerId(customerId);
delivery.setDeliveryNo("DEL" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + delivery.getId()); delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", dispatch.getOverseasWarehouseId(), delivery.getId()));
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo()); delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
delivery.setDispatchOrderId(dispatch.getId()); delivery.setDispatchNo(dispatch.getDispatchNo()); delivery.setDispatchOrderId(dispatch.getId()); delivery.setDispatchNo(dispatch.getDispatchNo());
delivery.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); delivery.setOverseasWarehouseName(dispatch.getOverseasWarehouseName()); delivery.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); delivery.setOverseasWarehouseName(dispatch.getOverseasWarehouseName());
@@ -197,14 +199,16 @@ public class DeliveryTransferService {
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim()); OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (existing != null) return taskResult(existing, pdaTasks.lockByOutbound(customerId, existing.getId())); if (existing != null) return taskResult(existing, pdaTasks.lockByOutbound(customerId, existing.getId()));
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
InventoryPosition firstPosition = inventoryPositions.selectById(request.items().get(0).inventoryPositionId());
if (firstPosition == null || !customerId.equals(firstPosition.getCustomerId())) throw notFound("Inventory position not found");
String selectedWarehouseId = firstPosition.getOverseasWarehouseId();
OutboundOrder outbound = new OutboundOrder(); OutboundOrder outbound = new OutboundOrder();
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId); outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
outbound.setOutboundNo("FBOO" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + outbound.getId()); outbound.setOutboundNo(orderNoGenerator.next("ONE_CLICK_DROPSHIP", selectedWarehouseId, outbound.getId()));
outbound.setOutboundType("ONE_CLICK_DROPSHIP"); outbound.setStatus("WAIT_PICK"); outbound.setRecipientInfo(request.recipient().trim()); outbound.setOutboundType("ONE_CLICK_DROPSHIP"); outbound.setStatus("WAIT_PICK"); outbound.setRecipientInfo(request.recipient().trim());
outbound.setPackageCount(request.items().stream().mapToInt(i -> i.packages().intValue()).sum()); outbound.setPalletCount(request.items().size()); outbound.setPackageCount(request.items().stream().mapToInt(i -> i.packages().intValue()).sum()); outbound.setPalletCount(request.items().size());
outbound.setIdempotencyKey(request.idempotencyKey().trim()); outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outbound.setIdempotencyKey(request.idempotencyKey().trim()); outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now);
outboundOrders.insert(outbound); outboundOrders.insert(outbound);
String selectedWarehouseId = null;
for (DeliveryRequests.OneClickItem input : request.items()) { for (DeliveryRequests.OneClickItem input : request.items()) {
InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId()); InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId());
if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found"); if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found");
@@ -44,6 +44,7 @@ import java.util.Set;
@Service @Service
public class CargoDiscardService { public class CargoDiscardService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
public static final String APPROVAL_PERMISSION = "lastleg:cargo-discard:approve"; public static final String APPROVAL_PERMISSION = "lastleg:cargo-discard:approve";
public static final String PENDING_REVIEW = "PENDING_REVIEW"; public static final String PENDING_REVIEW = "PENDING_REVIEW";
public static final String APPROVED = "APPROVED"; public static final String APPROVED = "APPROVED";
@@ -105,7 +106,7 @@ public class CargoDiscardService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
CargoDiscardApplication application = new CargoDiscardApplication(); CargoDiscardApplication application = new CargoDiscardApplication();
application.setId(IdWorker.getId()); application.setCustomerId(customerId); application.setId(IdWorker.getId()); application.setCustomerId(customerId);
application.setDiscardNo("FH" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + application.getId()); application.setDiscardNo(orderNoGenerator.next("DISCARD", warehouseId, application.getId()));
application.setOverseasWarehouseId(warehouseId); application.setOverseasWarehouseName(warehouseName); application.setOverseasWarehouseId(warehouseId); application.setOverseasWarehouseName(warehouseName);
application.setInboundCodes(orderCache.values().stream().map(LastlegOrder::getInboundCode) application.setInboundCodes(orderCache.values().stream().map(LastlegOrder::getInboundCode)
.distinct().sorted().collect(java.util.stream.Collectors.joining(","))); .distinct().sorted().collect(java.util.stream.Collectors.joining(",")));
@@ -31,6 +31,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -48,6 +49,7 @@ public class DispatchService {
@Resource private DispatchDraftMapper drafts; @Resource private DispatchDraftMapper drafts;
@Resource private DispatchDraftContainerMapper draftContainers; @Resource private DispatchDraftContainerMapper draftContainers;
@Resource private DispatchCandidateMapper candidates; @Resource private DispatchCandidateMapper candidates;
@Resource private LastlegOrderNoGenerator orderNoGenerator;
public String resolveCustomerScopeForPlans(List<Long> planIds) { public String resolveCustomerScopeForPlans(List<Long> planIds) {
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")), return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
@@ -239,7 +241,7 @@ public class DispatchService {
DispatchOrder dispatch = new DispatchOrder(); DispatchOrder dispatch = new DispatchOrder();
dispatch.setId(IdWorker.getId()); dispatch.setId(IdWorker.getId());
dispatch.setCustomerId(customerId); dispatch.setCustomerId(customerId);
dispatch.setDispatchNo("PC" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + dispatch.getId()); dispatch.setDispatchNo(orderNoGenerator.next("DISPATCH", selected.get(0).overseasWarehouseId(), dispatch.getId()));
dispatch.setOverseasWarehouseId(selected.get(0).overseasWarehouseId()); dispatch.setOverseasWarehouseId(selected.get(0).overseasWarehouseId());
dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName()); dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName());
dispatch.setRoute(StringUtils.hasText(request.route()) ? request.route().trim() : route(selected)); dispatch.setRoute(StringUtils.hasText(request.route()) ? request.route().trim() : route(selected));
@@ -68,4 +68,15 @@ public final class LastlegOrderRequests {
@Size(max = 500) String supplierDescription @Size(max = 500) String supplierDescription
) { ) {
} }
public record SetDetained(
@NotNull Boolean detained,
@NotBlank @Size(max = 500) String remark
) {
}
public record SetPalletMergeRequired(
@NotNull Boolean palletMergeRequired
) {
}
} }
@@ -31,6 +31,7 @@ import java.util.Objects;
@Service @Service
public class ExceptionOrderService { public class ExceptionOrderService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
@Resource private ExceptionOrderMapper exceptionOrders; @Resource private ExceptionOrderMapper exceptionOrders;
@Resource private LastlegOrderMapper lastMileOrders; @Resource private LastlegOrderMapper lastMileOrders;
@Resource private LastlegInboundOrderMapper inboundOrders; @Resource private LastlegInboundOrderMapper inboundOrders;
@@ -158,7 +159,7 @@ public class ExceptionOrderService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
ExceptionOrder value = new ExceptionOrder(); ExceptionOrder value = new ExceptionOrder();
value.setId(IdWorker.getId()); value.setCustomerId(relation.customerId()); value.setId(IdWorker.getId()); value.setCustomerId(relation.customerId());
value.setExceptionNo("EXC" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + value.getId()); value.setExceptionNo(orderNoGenerator.next("EXCEPTION", relation.overseasWarehouseId(), value.getId()));
value.setExceptionType(type); value.setStatus("PENDING"); value.setReason(trim(reason)); value.setExceptionType(type); value.setStatus("PENDING"); value.setReason(trim(reason));
value.setLastMileOrderId(relation.lastMileOrderId()); value.setLastMileOrderNo(relation.lastMileOrderNo()); value.setLastMileOrderId(relation.lastMileOrderId()); value.setLastMileOrderNo(relation.lastMileOrderNo());
value.setInboundOrderId(relation.inboundOrderId()); value.setInboundCode(relation.inboundCode()); value.setInboundOrderId(relation.inboundOrderId()); value.setInboundCode(relation.inboundCode());
@@ -26,14 +26,28 @@ public final class ReceivingContainerResponses {
String destinationWarehouseId, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName, String destinationWarehouseCode, String destinationWarehouseName,
String fbaWarehouseCode, int plannedPalletCount, int totalPackages, String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
int receivedPackages, int pendingPackages) { } int receivedPackages, int pendingPackages,
boolean palletMergeRequired, String palletMergeHint) { }
public record CartonResolved(String cartonMark, String inboundCode, public record CartonResolved(String cartonMark, String inboundCode,
String destinationType, String overseasWarehouseId, String destinationType, String overseasWarehouseId,
String overseasWarehouseName, String destinationWarehouseId, String overseasWarehouseName, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName, String destinationWarehouseCode, String destinationWarehouseName,
String fbaWarehouseCode, int plannedPalletCount, int totalPackages, String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
int receivedPackages, int pendingPackages) { } int receivedPackages, int pendingPackages,
boolean palletMergeRequired, String palletMergeHint) {
public CartonResolved(String cartonMark, String inboundCode, String destinationType,
String overseasWarehouseId, String overseasWarehouseName,
String destinationWarehouseId, String destinationWarehouseCode,
String destinationWarehouseName, String fbaWarehouseCode,
int plannedPalletCount, int totalPackages, int receivedPackages,
int pendingPackages) {
this(cartonMark, inboundCode, destinationType, overseasWarehouseId, overseasWarehouseName,
destinationWarehouseId, destinationWarehouseCode, destinationWarehouseName,
fbaWarehouseCode, plannedPalletCount, totalPackages, receivedPackages,
pendingPackages, false, null);
}
}
public record PalletResolved(String palletCode, long warehouseId, String status, public record PalletResolved(String palletCode, long warehouseId, String status,
String fbaWarehouseCode, String inboundCode, String fbaWarehouseCode, String inboundCode,
@@ -41,7 +55,15 @@ public final class ReceivingContainerResponses {
public record Receipt(String result, String containerNo, String inboundCode, public record Receipt(String result, String containerNo, String inboundCode,
String destinationType, String palletCode, int acceptedPackages, String destinationType, String palletCode, int acceptedPackages,
int receivedPackages, int pendingPackages, String sessionStatus) { } int receivedPackages, int pendingPackages, String sessionStatus,
boolean palletMergeRequired, String palletMergeHint) {
public Receipt(String result, String containerNo, String inboundCode,
String destinationType, String palletCode, int acceptedPackages,
int receivedPackages, int pendingPackages, String sessionStatus) {
this(result, containerNo, inboundCode, destinationType, palletCode, acceptedPackages,
receivedPackages, pendingPackages, sessionStatus, false, null);
}
}
public record Photo(String fileId, String fileName, String contentType, long sizeBytes) { } public record Photo(String fileId, String fileName, String contentType, long sizeBytes) { }
} }
@@ -224,15 +224,17 @@ public class InboundStockReceiptService {
String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId() + ":" + sku.skuCode(); String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId() + ":" + sku.skuCode();
String overseasWarehouseId = required(order.getOverseasWarehouseId(), String overseasWarehouseId = required(order.getOverseasWarehouseId(),
"Actual overseas warehouse is required for stock-in"); "Actual overseas warehouse is required for stock-in");
InventoryCommandResponse result = inventory.stockIn(customerId, task.getOperatorId(), InventoryCommandRequests.StockIn stockIn = new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId,
new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId,
null, order.getOverseasWarehouseName(), line.getDestinationWarehouseId(), destinationCode, null, order.getOverseasWarehouseName(), line.getDestinationWarehouseId(), destinationCode,
line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(), line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(),
sku.skuCode(), sku.productNameZh(), sku.productNameEn(), sku.skuCode(), sku.productNameZh(), sku.productNameEn(),
pallet.getWarehousePalletId(), pallet.getPalletNo(), plan.getPlanNo(), pallet.getWarehousePalletId(), pallet.getPalletNo(), plan.getPlanNo(),
pallet.getWarehouseLocationId(), null, packages, order.getOrderNo(), pallet.getWarehouseLocationId(), null, packages, order.getOrderNo(),
order.getSourceEventId(), operationKey, order.getSourceEventId(), operationKey,
"PDA inbound completion for " + line.getInboundCode())); "PDA inbound completion for " + line.getInboundCode());
InventoryCommandResponse result = Boolean.TRUE.equals(order.getDetained())
? inventory.stockInHeld(customerId, task.getOperatorId(), stockIn)
: inventory.stockIn(customerId, task.getOperatorId(), stockIn);
InboundStockReceipt receipt = new InboundStockReceipt(); InboundStockReceipt receipt = new InboundStockReceipt();
receipt.setId(IdWorker.getId()); receipt.setId(IdWorker.getId());
@@ -61,6 +61,7 @@ import java.util.stream.Collectors;
public class ReceivingContainerService { public class ReceivingContainerService {
private static final String PHOTO_DOWNLOAD_PATH = private static final String PHOTO_DOWNLOAD_PATH =
"/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files/{fileId}/download"; "/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files/{fileId}/download";
private static final String PALLET_MERGE_HINT = "该入仓单已标记合托,请按合托要求拣货";
@Resource private InboundPlanMapper plans; @Resource private InboundPlanMapper plans;
@Resource private InboundPlanLineMapper lines; @Resource private InboundPlanLineMapper lines;
@@ -245,7 +246,8 @@ public class ReceivingContainerService {
inbound.inboundCode(), inbound.destinationType(), inbound.overseasWarehouseId(), inbound.inboundCode(), inbound.destinationType(), inbound.overseasWarehouseId(),
inbound.overseasWarehouseName(), inbound.destinationWarehouseId(), inbound.overseasWarehouseName(), inbound.destinationWarehouseId(),
inbound.destinationWarehouseCode(), inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(), inbound.destinationWarehouseCode(), inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(),
inbound.plannedPalletCount(), inbound.totalPackages(), inbound.receivedPackages(), inbound.pendingPackages()); inbound.plannedPalletCount(), inbound.totalPackages(), inbound.receivedPackages(), inbound.pendingPackages(),
inbound.palletMergeRequired(), inbound.palletMergeHint());
} }
public ReceivingContainerResponses.PalletResolved resolvePallet(String operatorKey, public ReceivingContainerResponses.PalletResolved resolvePallet(String operatorKey,
@@ -290,7 +292,8 @@ public class ReceivingContainerService {
int total = fbaPlanRefs.stream().mapToInt(refValue -> value(refValue.line().getRequiredPackageCount())).sum(); int total = fbaPlanRefs.stream().mapToInt(refValue -> value(refValue.line().getRequiredPackageCount())).sum();
ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED", ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED",
session.getContainerNo(), ref.line().getInboundCode(), "FBA", resource.palletNo(), accepted, session.getContainerNo(), ref.line().getInboundCode(), "FBA", resource.palletNo(), accepted,
received, Math.max(0, total - received), session.getStatus()); received, Math.max(0, total - received), session.getStatus(), palletMergeRequired(ref.order()),
palletMergeHint(ref.order()));
save(session, "BULK_RECEIVE", request.idempotencyKey(), request, result); save(session, "BULK_RECEIVE", request.idempotencyKey(), request, result);
return result; return result;
} catch (RuntimeException failure) { } catch (RuntimeException failure) {
@@ -328,7 +331,8 @@ public class ReceivingContainerService {
ReceivingContainerResponses.Inbound inbound = inbound(ref); ReceivingContainerResponses.Inbound inbound = inbound(ref);
ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED", ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED",
session.getContainerNo(), inbound.inboundCode(), inbound.destinationType(), resource.palletNo(), 1, session.getContainerNo(), inbound.inboundCode(), inbound.destinationType(), resource.palletNo(), 1,
inbound.receivedPackages(), inbound.pendingPackages(), session.getStatus()); inbound.receivedPackages(), inbound.pendingPackages(), session.getStatus(),
inbound.palletMergeRequired(), inbound.palletMergeHint());
save(session, "CARTON_RECEIVE", request.idempotencyKey(), request, result); save(session, "CARTON_RECEIVE", request.idempotencyKey(), request, result);
return result; return result;
} catch (RuntimeException failure) { } catch (RuntimeException failure) {
@@ -539,7 +543,17 @@ public class ReceivingContainerService {
: String.valueOf(ref.plan().getOverseasWarehouseId()), ref.order().getOverseasWarehouseName(), : String.valueOf(ref.plan().getOverseasWarehouseId()), ref.order().getOverseasWarehouseName(),
ref.line().getDestinationWarehouseId(), ref.line().getDestinationWarehouseCode(), ref.line().getDestinationWarehouseId(), ref.line().getDestinationWarehouseCode(),
ref.line().getDestinationWarehouseName(), ref.line().getFbaWarehouseCode(), ref.line().getDestinationWarehouseName(), ref.line().getFbaWarehouseCode(),
value(ref.line().getPlannedPalletCount()), total, received, Math.max(0, total - received)); value(ref.line().getPlannedPalletCount()), total, received, Math.max(0, total - received),
palletMergeRequired(ref.order()), palletMergeHint(ref.order()));
}
private boolean palletMergeRequired(LastlegOrder order) {
return order != null && ("REQUIRED".equals(order.getPalletMergeFlag())
|| "MERGED".equals(order.getPalletMergeFlag()));
}
private String palletMergeHint(LastlegOrder order) {
return palletMergeRequired(order) ? PALLET_MERGE_HINT : null;
} }
private Totals totals(List<InboundPlan> containerPlans) { private Totals totals(List<InboundPlan> containerPlans) {
@@ -36,6 +36,7 @@ import java.util.stream.Collectors;
@Service @Service
public class InboundPlanService { public class InboundPlanService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
@Resource private InboundPlanMapper planMapper; @Resource private InboundPlanMapper planMapper;
@Resource private InboundPlanLineMapper lineMapper; @Resource private InboundPlanLineMapper lineMapper;
@Resource private InboundPlanPalletMapper palletMapper; @Resource private InboundPlanPalletMapper palletMapper;
@@ -106,7 +107,7 @@ public class InboundPlanService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
InboundPlan plan = new InboundPlan(); InboundPlan plan = new InboundPlan();
plan.setId(IdWorker.getId()); plan.setId(IdWorker.getId());
plan.setPlanNo("LIP" + plan.getId()); plan.setPlanNo(orderNoGenerator.next("INBOUND_PLAN", orders.get(0).destinationWarehouseCode(), plan.getId()));
plan.setCustomerId(customerId); plan.setCustomerId(customerId);
plan.setSourceOrderId(selectedOrderId); plan.setSourceOrderId(selectedOrderId);
plan.setContainerNo(containerNo); plan.setContainerNo(containerNo);
@@ -31,4 +31,10 @@ public interface InventoryCommandMapper {
List<InventoryBalance> lockInboundTransitBalances(@Param("customerId") String customerId, List<InventoryBalance> lockInboundTransitBalances(@Param("customerId") String customerId,
@Param("inboundOrderId") Long inboundOrderId, @Param("inboundOrderId") Long inboundOrderId,
@Param("inboundCode") String inboundCode); @Param("inboundCode") String inboundCode);
@Select("SELECT p.* FROM ll_inventory_position p JOIN ll_inventory_balance b ON b.id=p.balance_id " +
"WHERE p.customer_id=#{customerId} AND b.inbound_order_id=#{inboundOrderId} " +
"AND p.hold_packages>0 ORDER BY p.id FOR UPDATE")
List<InventoryPosition> lockInboundHeldPositions(@Param("customerId") String customerId,
@Param("inboundOrderId") Long inboundOrderId);
} }
@@ -60,13 +60,25 @@ public class InventoryCommandService {
@Transactional @Transactional
public InventoryCommandResponse stockIn(String customerId, String operatorId, public InventoryCommandResponse stockIn(String customerId, String operatorId,
InventoryCommandRequests.StockIn request) { InventoryCommandRequests.StockIn request) {
return stockIn(customerId, operatorId, request, false);
}
@Transactional
public InventoryCommandResponse stockInHeld(String customerId, String operatorId,
InventoryCommandRequests.StockIn request) {
return stockIn(customerId, operatorId, request, true);
}
private InventoryCommandResponse stockIn(String customerId, String operatorId,
InventoryCommandRequests.StockIn request, boolean held) {
return execute(customerId, operatorId, "STOCK_IN", request.idempotencyKey(), request, operation -> { return execute(customerId, operatorId, "STOCK_IN", request.idempotencyKey(), request, operation -> {
List<InventoryBalance> transitBalances = commands.lockInboundTransitBalances(customerId, List<InventoryBalance> transitBalances = commands.lockInboundTransitBalances(customerId,
request.inboundOrderId(), request.inboundCode()); request.inboundOrderId(), request.inboundCode());
InventoryBalance balance = balance(customerId, request); InventoryBalance balance = balance(customerId, request);
InventoryPosition position = position(customerId, balance, request); InventoryPosition position = position(customerId, balance, request);
long before = value(position.getAvailablePackages()); long before = held ? value(position.getHoldPackages()) : value(position.getAvailablePackages());
position.setAvailablePackages(before + request.packages()); if (held) position.setHoldPackages(before + request.packages());
else position.setAvailablePackages(before + request.packages());
position.setStatus("OCCUPIED"); position.setStatus("OCCUPIED");
position.setVersion(value(position.getVersion()) + 1); position.setVersion(value(position.getVersion()) + 1);
position.setUpdatedAt(LocalDateTime.now()); position.setUpdatedAt(LocalDateTime.now());
@@ -74,11 +86,12 @@ public class InventoryCommandService {
long transferred = transferInTransit(customerId, operatorId, operation.getId(), balance, long transferred = transferInTransit(customerId, operatorId, operation.getId(), balance,
transitBalances, request); transitBalances, request);
balance.setAvailablePackages(value(balance.getAvailablePackages()) + request.packages()); if (held) balance.setHoldPackages(value(balance.getHoldPackages()) + request.packages());
else balance.setAvailablePackages(value(balance.getAvailablePackages()) + request.packages());
touch(balance); touch(balance);
balances.updateById(balance); balances.updateById(balance);
ledger(customerId, operatorId, operation.getId(), balance, position, sku(request.skuCode()), "STOCK_IN", ledger(customerId, operatorId, operation.getId(), balance, position, sku(request.skuCode()), "STOCK_IN",
transferred == request.packages() ? "IN_TRANSIT" : "OUTSIDE", "AVAILABLE", transferred == request.packages() ? "IN_TRANSIT" : "OUTSIDE", held ? "HOLD" : "AVAILABLE",
before, request.packages(), before + request.packages(), before, request.packages(), before + request.packages(),
request.businessOrderNo(), request.inboundOrderNo(), null, request.sourceEventId(), request.businessOrderNo(), request.inboundOrderNo(), null, request.sourceEventId(),
request.idempotencyKey(), request.remark()); request.idempotencyKey(), request.remark());
@@ -86,6 +99,17 @@ public class InventoryCommandService {
}); });
} }
@Transactional
public void releaseInboundOrderHold(String customerId, String operatorId, long inboundOrderId,
String businessOrderNo, String remark) {
for (InventoryPosition position : commands.lockInboundHeldPositions(customerId, inboundOrderId)) {
long packages = value(position.getHoldPackages());
if (packages == 0) continue;
release(customerId, operatorId, new InventoryCommandRequests.Move(position.getId(), packages,
businessOrderNo, null, null, "ORDER_HOLD_RELEASE:" + inboundOrderId + ":" + position.getId(), remark));
}
}
private long transferInTransit(String customerId, String operatorId, Long operationId, private long transferInTransit(String customerId, String operatorId, Long operationId,
InventoryBalance target, List<InventoryBalance> sources, InventoryBalance target, List<InventoryBalance> sources,
InventoryCommandRequests.StockIn request) { InventoryCommandRequests.StockIn request) {
@@ -70,7 +70,7 @@ public class LocalLooseboardService {
LocalLooseboardOrder order = new LocalLooseboardOrder(); LocalLooseboardOrder order = new LocalLooseboardOrder();
order.setId(IdWorker.getId()); order.setId(IdWorker.getId());
order.setCustomerId(customerId); order.setCustomerId(customerId);
order.setOrderNo("LB" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + order.getId()); order.setOrderNo(lastlegOrderNoGenerator.next("LOOSEBOARD", request.destination(), order.getId()));
apply(order, request.transportMode(), request.palletRequired(), request.transportType(), request.origin(), apply(order, request.transportMode(), request.palletRequired(), request.transportType(), request.origin(),
request.originAddress(), request.destination(), request.destinationAddress(), request.containerType(), request.originAddress(), request.destination(), request.destinationAddress(), request.containerType(),
request.packageCount(), request.totalWeightKg(), request.totalVolumeCbm(), request.budgetAmount(), request.packageCount(), request.totalWeightKg(), request.totalVolumeCbm(), request.budgetAmount(),
@@ -257,7 +257,7 @@ public class LocalLooseboardService {
private LastlegOrder createLastlegOrder(LocalLooseboardOrder source, String requestHash, LocalDateTime now) { private LastlegOrder createLastlegOrder(LocalLooseboardOrder source, String requestHash, LocalDateTime now) {
LastlegOrder order = new LastlegOrder(); LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId()); order.setId(IdWorker.getId());
order.setOrderNo(lastlegOrderNoGenerator.next()); order.setOrderNo(lastlegOrderNoGenerator.next("LOOSEBOARD", source.getDestination(), order.getId()));
order.setCustomerId(source.getCustomerId()); order.setCustomerId(source.getCustomerId());
order.setOrderType("LOCAL_LOOSEBOARD"); order.setOrderType("LOCAL_LOOSEBOARD");
order.setInboundCode(source.getOrderNo()); order.setInboundCode(source.getOrderNo());
@@ -42,4 +42,10 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
int claimReceivingWarehouse(@Param("customerId") String customerId, @Param("orderId") long orderId, int claimReceivingWarehouse(@Param("customerId") String customerId, @Param("orderId") long orderId,
@Param("warehouseId") String warehouseId, @Param("warehouseId") String warehouseId,
@Param("warehouseName") String warehouseName); @Param("warehouseName") String warehouseName);
@Update("UPDATE ll_order SET pallet_merge_flag=#{flag},updated_at=#{updatedAt} " +
"WHERE customer_id=#{customerId} AND id=#{id}")
int updatePalletMergeFlag(@Param("customerId") String customerId, @Param("id") long id,
@Param("flag") String flag,
@Param("updatedAt") java.time.LocalDateTime updatedAt);
} }
@@ -37,6 +37,7 @@ import java.util.Set;
@Service @Service
public class OverseasProcurementService { public class OverseasProcurementService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
public static final String REVIEW_PERMISSION = OverseasPermissions.PURCHASE_REVIEW; public static final String REVIEW_PERMISSION = OverseasPermissions.PURCHASE_REVIEW;
public static final String MANAGE_PERMISSION = OverseasPermissions.PURCHASE_EDIT; public static final String MANAGE_PERMISSION = OverseasPermissions.PURCHASE_EDIT;
public static final String PENDING_REVIEW = "PENDING_REVIEW"; public static final String PENDING_REVIEW = "PENDING_REVIEW";
@@ -62,7 +63,7 @@ public class OverseasProcurementService {
order.setId(IdWorker.getId()); order.setId(IdWorker.getId());
order.setCustomerId(customerId); order.setCustomerId(customerId);
order.setCustomerName(trim(request.customerName())); order.setCustomerName(trim(request.customerName()));
order.setOrderNo("HC" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + order.getId()); order.setOrderNo(orderNoGenerator.next("PROCUREMENT", request.receivingAddress(), order.getId()));
order.setPurchaseCategory(request.purchaseCategory().trim()); order.setPurchaseCategory(request.purchaseCategory().trim());
order.setProductName(request.productName().trim()); order.setProductName(request.productName().trim());
order.setProductSpecification(trim(request.productSpecification())); order.setProductSpecification(trim(request.productSpecification()));
@@ -67,10 +67,10 @@ public class RelabelApplicationService {
} }
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
long applicationId = IdWorker.getId(); long applicationId = IdWorker.getId();
String relabelNo = "FBR" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + applicationId; String relabelNo = orderNoGenerator.next("RELABEL", request.overseasWarehouseId(), applicationId);
LastlegOrder order = new LastlegOrder(); LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId()); order.setId(IdWorker.getId());
order.setOrderNo(orderNoGenerator.next()); order.setOrderNo(orderNoGenerator.next("RELABEL", request.overseasWarehouseId(), order.getId()));
order.setCustomerId(customerId); order.setCustomerId(customerId);
order.setOrderType("RELABEL_APPLICATION"); order.setOrderType("RELABEL_APPLICATION");
order.setInboundCode(relabelNo); order.setInboundCode(relabelNo);
@@ -3,15 +3,59 @@ package com.baiyue.lastleg.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
@Component @Component
public class LastlegOrderNoGenerator { public class LastlegOrderNoGenerator {
public String next() { public String next() {
return "LMO" + IdWorker.getIdStr(); long id = IdWorker.getId();
return next("LMO", null, id);
}
public String next(String businessType, String warehouse, long uniqueId) {
String type = normalizeType(businessType);
return type + "-" + warehouseCode(warehouse) + "-"
+ LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"
+ String.format(Locale.ROOT, "%06d", Math.floorMod(uniqueId, 1_000_000));
}
private String normalizeType(String value) {
String code = value == null ? "LMO" : value.trim().toUpperCase(Locale.ROOT);
return switch (code) {
case "FIRST_LEG" -> "FL";
case "ONE_CLICK_DROPSHIP" -> "OD";
case "RELABEL_APPLICATION", "RELABEL" -> "RL";
case "WAREHOUSE_TRANSFER", "TRANSFER" -> "WT";
case "LOCAL_LOOSEBOARD", "LOOSEBOARD" -> "LB";
case "DISPATCH", "PICKUP_CAR" -> "DS";
case "OUTBOUND" -> "OB";
case "DELIVERY" -> "DL";
case "TRUCK" -> "TR";
case "EXCEPTION" -> "EX";
case "DISCARD" -> "CD";
case "PROCUREMENT" -> "PR";
case "INBOUND_PLAN" -> "IP";
default -> {
String normalized = code.replaceAll("[^A-Z0-9]", "");
yield normalized.isEmpty() ? "LMO" : normalized.substring(0, Math.min(4, normalized.length()));
}
};
}
public String warehouseCode(String value) {
String code = value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
if (code.isEmpty()) return "WH";
if (code.matches(".*(LAX|ONT|NYC|JFK|LGB|EWR|SFO|SEA|DFW|ORD|ATL|MIA|PHX|LAS|YYZ|YVR|AMS|LHR|FRA|CDG|HKG|NRT|SYD).*")) {
var match = java.util.regex.Pattern.compile("(LAX|ONT|NYC|JFK|LGB|EWR|SFO|SEA|DFW|ORD|ATL|MIA|PHX|LAS|YYZ|YVR|AMS|LHR|FRA|CDG|HKG|NRT|SYD)").matcher(code);
if (match.find()) return match.group(1);
}
if (code.matches("[A-Z0-9]{2,8}")) return code;
return "WH";
} }
/** The physical container number is the tail-leg business order grouping boundary. */ /** The physical container number is the tail-leg business order grouping boundary. */
@@ -20,14 +64,11 @@ public class LastlegOrderNoGenerator {
throw new IllegalArgumentException("containerNo is required"); throw new IllegalArgumentException("containerNo is required");
} }
String normalized = containerNo.trim(); String normalized = containerNo.trim();
if (normalized.matches("[0-9]{1,29}")) {
return "LMO" + normalized;
}
try { try {
byte[] digest = MessageDigest.getInstance("SHA-256") byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(normalized.getBytes(StandardCharsets.UTF_8)); .digest(normalized.getBytes(StandardCharsets.UTF_8));
String digits = new BigInteger(1, digest).toString(); long suffix = java.nio.ByteBuffer.wrap(digest, 0, 8).getLong();
return "LMO" + digits.substring(0, Math.min(29, digits.length())); return next("FIRST_LEG", null, suffix);
} catch (NoSuchAlgorithmException exception) { } catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable", exception); throw new IllegalStateException("SHA-256 is unavailable", exception);
} }
@@ -14,6 +14,7 @@ import com.baiyue.lastleg.entity.LastlegOrderFee;
import com.baiyue.lastleg.entity.LastlegOrderOperationLog; import com.baiyue.lastleg.entity.LastlegOrderOperationLog;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton; import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; 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.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
@@ -38,6 +39,8 @@ public class LastlegOrderService {
public static final String PENDING_ASSIGNMENT = "PENDING_ASSIGNMENT"; public static final String PENDING_ASSIGNMENT = "PENDING_ASSIGNMENT";
public static final String DESTINATION_ASSIGNED = "DESTINATION_ASSIGNED"; public static final String DESTINATION_ASSIGNED = "DESTINATION_ASSIGNED";
public static final String INBOUND_PLANNED = "INBOUND_PLANNED"; public static final String INBOUND_PLANNED = "INBOUND_PLANNED";
public static final String PALLET_MERGE_REQUIRED = "REQUIRED";
public static final String PALLET_MERGED = "MERGED";
@Resource private LastlegOrderMapper orderMapper; @Resource private LastlegOrderMapper orderMapper;
@Resource private LastlegOrderDetailMapper detailMapper; @Resource private LastlegOrderDetailMapper detailMapper;
@@ -45,6 +48,7 @@ public class LastlegOrderService {
@Resource private LastlegOrderFeeMapper feeMapper; @Resource private LastlegOrderFeeMapper feeMapper;
@Resource private LastlegOrderOperationLogMapper operationLogMapper; @Resource private LastlegOrderOperationLogMapper operationLogMapper;
@Resource private InboundPlanCartonMapper cartonMapper; @Resource private InboundPlanCartonMapper cartonMapper;
@Resource private InventoryCommandService inventory;
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) { public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
Page<LastlegOrder> page = orderMapper.selectPage( Page<LastlegOrder> page = orderMapper.selectPage(
@@ -110,6 +114,70 @@ public class LastlegOrderService {
return fee; return fee;
} }
@Transactional
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained 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");
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);
}
@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 @Transactional
public int transferDestination(String customerId, LastlegOrderRequests.TransferDestination request) { public int transferDestination(String customerId, LastlegOrderRequests.TransferDestination request) {
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList(); List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
@@ -83,8 +83,8 @@ public class LoadingCompletedConsumer {
shippingGates.initialize(payload.containerBookingId(), envelope.tenantId(), "", shippingGates.initialize(payload.containerBookingId(), envelope.tenantId(), "",
payload.containerNo(), 0, envelope.handoverVersion()); payload.containerNo(), 0, envelope.handoverVersion());
String orderNo = orderNoGenerator.forContainerNo(payload.containerNo());
JsonNode rawOrders = envelope.payload().path("orders"); JsonNode rawOrders = envelope.payload().path("orders");
String orderNo = orderNoGenerator.forContainerNo(payload.containerNo());
for (int index = 0; index < payload.orders().size(); index++) { for (int index = 0; index < payload.orders().size(); index++) {
var source = payload.orders().get(index); var source = payload.orders().get(index);
JsonNode rawSource = rawOrders.isArray() && index < rawOrders.size() ? rawOrders.get(index) : objectMapper.createObjectNode(); JsonNode rawSource = rawOrders.isArray() && index < rawOrders.size() ? rawOrders.get(index) : objectMapper.createObjectNode();
@@ -117,7 +117,7 @@ public class WarehouseTransferService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
long transferId = IdWorker.getId(); long transferId = IdWorker.getId();
String transferNo = "WTR" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + transferId; String transferNo = orderNoGenerator.next("TRANSFER", request.operatingWarehouseId(), transferId);
LastlegOrder order = createTailOrder(customerId, inboundOrderId, transferNo, request, requestHash, now); LastlegOrder order = createTailOrder(customerId, inboundOrderId, transferNo, request, requestHash, now);
WarehouseTransferOrder transfer = new WarehouseTransferOrder(); WarehouseTransferOrder transfer = new WarehouseTransferOrder();
@@ -464,7 +464,7 @@ public class WarehouseTransferService {
private LastlegOrder createTailOrder(String customerId, Long inboundOrderId, String transferNo, private LastlegOrder createTailOrder(String customerId, Long inboundOrderId, String transferNo,
WarehouseTransferRequests.Create request, String requestHash, LocalDateTime now) { WarehouseTransferRequests.Create request, String requestHash, LocalDateTime now) {
LastlegOrder order = new LastlegOrder(); LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId()); order.setOrderNo(orderNoGenerator.next()); order.setCustomerId(customerId); order.setId(IdWorker.getId()); order.setOrderNo(orderNoGenerator.next("TRANSFER", request.operatingWarehouseId(), order.getId())); order.setCustomerId(customerId);
order.setOrderType(request.needRelabel() ? RETURN_RELABEL_ORDER_TYPE : ORDER_TYPE); order.setOrderType(request.needRelabel() ? RETURN_RELABEL_ORDER_TYPE : ORDER_TYPE);
order.setInboundCode(request.inboundCode().trim()); order.setSourceRegion("CUSTOMER"); order.setInboundCode(request.inboundCode().trim()); order.setSourceRegion("CUSTOMER");
order.setSourceInboundId("WAREHOUSE_TRANSFER:" + transferNo); order.setRootInboundId(String.valueOf(inboundOrderId)); order.setSourceInboundId("WAREHOUSE_TRANSFER:" + transferNo); order.setRootInboundId(String.valueOf(inboundOrderId));
@@ -505,7 +505,7 @@ public class WarehouseTransferService {
private void createRelabel(String customerId, String operatorId, WarehouseTransferOrder transfer, LastlegOrder order, private void createRelabel(String customerId, String operatorId, WarehouseTransferOrder transfer, LastlegOrder order,
WarehouseTransferRequests.Create request, LocalDateTime now) { WarehouseTransferRequests.Create request, LocalDateTime now) {
long applicationId = IdWorker.getId(); long applicationId = IdWorker.getId();
String relabelNo = "FBR" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + applicationId; String relabelNo = orderNoGenerator.next("RELABEL", request.operatingWarehouseId(), applicationId);
RelabelApplication application = new RelabelApplication(); RelabelApplication application = new RelabelApplication();
application.setId(applicationId); application.setCustomerId(customerId); application.setRelabelNo(relabelNo); application.setId(applicationId); application.setCustomerId(customerId); application.setRelabelNo(relabelNo);
application.setLastlegOrderId(order.getId()); application.setCustomerOrderNo(transfer.getTransferNo()); application.setLastlegOrderId(order.getId()); application.setCustomerOrderNo(transfer.getTransferNo());
@@ -39,6 +39,7 @@ import java.util.function.Consumer;
@Service @Service
public class UsTruckOrderService { public class UsTruckOrderService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
public static final String MANAGE_PERMISSION = OverseasPermissions.TRUCK_STATUS; public static final String MANAGE_PERMISSION = OverseasPermissions.TRUCK_STATUS;
public static final String PENDING_REVIEW = "PENDING_REVIEW"; public static final String PENDING_REVIEW = "PENDING_REVIEW";
/** Kept for source compatibility; new orders use PENDING_REVIEW. */ /** Kept for source compatibility; new orders use PENDING_REVIEW. */
@@ -79,7 +80,7 @@ public class UsTruckOrderService {
UsTruckOrder order = new UsTruckOrder(); UsTruckOrder order = new UsTruckOrder();
order.setId(IdWorker.getId()); order.setId(IdWorker.getId());
order.setCustomerId(customerId); order.setCustomerId(customerId);
order.setOrderNo("TK" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + order.getId()); order.setOrderNo(orderNoGenerator.next("TRUCK", request.origin(), order.getId()));
order.setTransportType(StringUtils.hasText(request.transportType()) ? norm(request.transportType()) : null); order.setTransportType(StringUtils.hasText(request.transportType()) ? norm(request.transportType()) : null);
order.setOrigin(trim(request.origin())); order.setOriginAddress(trim(request.originAddress())); order.setOrigin(trim(request.origin())); order.setOriginAddress(trim(request.originAddress()));
order.setDestination(trim(request.destination())); order.setDestinationAddress(trim(request.destinationAddress())); order.setDestination(trim(request.destination())); order.setDestinationAddress(trim(request.destinationAddress()));
@@ -179,6 +179,25 @@ paths:
responses: responses:
'200': { description: 新增后的费用明细, content: { application/json: { schema: { $ref: '#/components/schemas/LastMileFeeApiResponse' } } } } '200': { description: 新增后的费用明细, content: { application/json: { schema: { $ref: '#/components/schemas/LastMileFeeApiResponse' } } } }
'404': { description: 尾程订单不存在或不属于操作客户 } '404': { description: 尾程订单不存在或不属于操作客户 }
/api/platform/v1/last-leg/orders/{id}/pallet-merge-required:
put:
summary: 设置尾程订单合托标记
description: 权限 platform:overseas-warehouse:last-mile-order:edit;标记后,PDA扫描本单箱唛并解析入仓单时返回合托提示;实际完成合托后不可取消。
security: [{ bearerAuth: [] }]
parameters:
- { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [palletMergeRequired]
properties: { palletMergeRequired: { type: boolean } }
responses:
'200': { description: 合托标记已更新 }
'404': { description: 尾程订单不存在或不属于操作客户 }
'409': { description: 入仓已完成或实际合托已完成,不能修改标记 }
/api/platform/v1/last-leg/dispatches: /api/platform/v1/last-leg/dispatches:
get: get:
summary: 分页查询排车单 summary: 分页查询排车单
@@ -729,7 +748,7 @@ components:
timestamp: { type: string, format: date-time } timestamp: { type: string, format: date-time }
ReceivingInbound: ReceivingInbound:
type: object type: object
required: [inboundCode, destinationType, plannedPalletCount, totalPackages, receivedPackages, pendingPackages] required: [inboundCode, destinationType, plannedPalletCount, totalPackages, receivedPackages, pendingPackages, palletMergeRequired]
properties: properties:
inboundCode: { type: string } inboundCode: { type: string }
destinationType: { type: string, enum: [FBA, OVERSEAS] } destinationType: { type: string, enum: [FBA, OVERSEAS] }
@@ -743,6 +762,8 @@ components:
totalPackages: { type: integer } totalPackages: { type: integer }
receivedPackages: { type: integer } receivedPackages: { type: integer }
pendingPackages: { type: integer } pendingPackages: { type: integer }
palletMergeRequired: { type: boolean, description: 尾程订单是否要求PDA合托提醒 }
palletMergeHint: { type: string, nullable: true, description: 标记合托时供PDA直接展示的中文提示 }
ReceivingWarehouseProfile: ReceivingWarehouseProfile:
type: object type: object
required: [warehouseId, warehouseCode, warehouseName] required: [warehouseId, warehouseCode, warehouseName]
@@ -810,7 +831,7 @@ components:
compatible: { type: boolean } compatible: { type: boolean }
ReceivingReceipt: ReceivingReceipt:
type: object type: object
required: [result, containerNo, inboundCode, destinationType, palletCode, acceptedPackages, receivedPackages, pendingPackages, sessionStatus] required: [result, containerNo, inboundCode, destinationType, palletCode, acceptedPackages, receivedPackages, pendingPackages, sessionStatus, palletMergeRequired]
properties: properties:
result: { type: string } result: { type: string }
containerNo: { type: string } containerNo: { type: string }
@@ -821,6 +842,8 @@ components:
receivedPackages: { type: integer } receivedPackages: { type: integer }
pendingPackages: { type: integer } pendingPackages: { type: integer }
sessionStatus: { type: string, enum: [SIGNED, RECEIVING] } sessionStatus: { type: string, enum: [SIGNED, RECEIVING] }
palletMergeRequired: { type: boolean }
palletMergeHint: { type: string, nullable: true }
ReceivingCandidateListApiResponse: ReceivingCandidateListApiResponse:
allOf: allOf:
- { $ref: '#/components/schemas/ReceivingApiResponseBase' } - { $ref: '#/components/schemas/ReceivingApiResponseBase' }
@@ -22,6 +22,7 @@ import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper; import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -73,6 +74,7 @@ class DeliveryTransferServiceTest {
ReflectionTestUtils.setField(service, "deliveryOperations", deliveryOperations); ReflectionTestUtils.setField(service, "deliveryOperations", deliveryOperations);
ReflectionTestUtils.setField(service, "planAllocations", planAllocations); ReflectionTestUtils.setField(service, "planAllocations", planAllocations);
ReflectionTestUtils.setField(service, "planLines", planLines); ReflectionTestUtils.setField(service, "planLines", planLines);
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
ReflectionTestUtils.setField(service, "tailOrders", tailOrders); ReflectionTestUtils.setField(service, "tailOrders", tailOrders);
ReflectionTestUtils.setField(service, "histories", histories); ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "tailOrderDetails", tailOrderDetails); ReflectionTestUtils.setField(service, "tailOrderDetails", tailOrderDetails);
@@ -102,10 +104,10 @@ class DeliveryTransferServiceTest {
assertEquals(2, outbound.getValue().getPalletCount()); assertEquals(2, outbound.getValue().getPalletCount());
assertEquals(20, outbound.getValue().getPackageCount()); assertEquals(20, outbound.getValue().getPackageCount());
assertEquals("transfer-101", outbound.getValue().getIdempotencyKey()); assertEquals("transfer-101", outbound.getValue().getIdempotencyKey());
assertTrue(outbound.getValue().getOutboundNo().startsWith("OUT")); assertTrue(outbound.getValue().getOutboundNo().matches("OB-WH-\\d{8}-\\d{6}"));
assertEquals(outbound.getValue().getId(), delivery.getValue().getOutboundOrderId()); assertEquals(outbound.getValue().getId(), delivery.getValue().getOutboundOrderId());
assertEquals("WAIT_DELIVERY", delivery.getValue().getStatus()); assertEquals("WAIT_DELIVERY", delivery.getValue().getStatus());
assertTrue(delivery.getValue().getDeliveryNo().startsWith("DEL")); assertTrue(delivery.getValue().getDeliveryNo().matches("DL-WH-\\d{8}-\\d{6}"));
assertEquals(List.of("ONT8", "LAX9"), pallets.getAllValues().stream() assertEquals(List.of("ONT8", "LAX9"), pallets.getAllValues().stream()
.map(OutboundPalletItem::getDestinationWarehouseCode).toList()); .map(OutboundPalletItem::getDestinationWarehouseCode).toList());
assertEquals(List.of("IN-A", "IN-B"), pallets.getAllValues().stream() assertEquals(List.of("IN-A", "IN-B"), pallets.getAllValues().stream()
@@ -14,6 +14,7 @@ import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory; import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
import com.baiyue.lastleg.entity.LastlegOrder; import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -63,6 +64,7 @@ class CargoDiscardServiceTest {
ReflectionTestUtils.setField(service, "histories", histories); ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "inventory", inventory); ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "adjustments", adjustments); ReflectionTestUtils.setField(service, "adjustments", adjustments);
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
ReflectionTestUtils.setField(service, "orders", orders); ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper()); ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00")); ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00"));
@@ -56,6 +56,7 @@ class DispatchServiceTest {
ReflectionTestUtils.setField(service, "candidates", candidates); ReflectionTestUtils.setField(service, "candidates", candidates);
ReflectionTestUtils.setField(service, "drafts", drafts); ReflectionTestUtils.setField(service, "drafts", drafts);
ReflectionTestUtils.setField(service, "draftContainers", draftContainers); ReflectionTestUtils.setField(service, "draftContainers", draftContainers);
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
lenient().when(items.findByDispatchOrder(any(), any())).thenReturn(List.of()); lenient().when(items.findByDispatchOrder(any(), any())).thenReturn(List.of());
} }
@@ -155,6 +155,21 @@ class InboundStockReceiptServiceTest {
assertEquals(5L, requests.getAllValues().stream().mapToLong(InventoryCommandRequests.StockIn::packages).sum()); assertEquals(5L, requests.getAllValues().stream().mapToLong(InventoryCommandRequests.StockIn::packages).sum());
} }
@Test
void stocksInToHeldInventoryWhenInboundOrderIsDetained() {
Fixture fixture = fixture(5, 5, 5, 10);
fixture.order.setDetained(true);
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(inventory.stockInHeld(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now());
verify(inventory).stockInHeld(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class));
verify(inventory, never()).stockIn(any(), any(), any());
}
@Test @Test
void existingTaskReceiptMakesStockInReplayANoOp() { void existingTaskReceiptMakesStockInReplayANoOp() {
Fixture fixture = fixture(10, 5, 5, 10); Fixture fixture = fixture(10, 5, 5, 10);
@@ -422,6 +422,20 @@ class ReceivingContainerServiceBehaviorTest {
verifyNoInteractions(workflow, operations); verifyNoInteractions(workflow, operations);
} }
@Test
void cartonResolutionReturnsInboundAndPalletMergeReminderFromTailLegOrder() {
stubCurrent("FBA", "CONT-A");
LastlegOrder marked = order("FBA");
marked.setPalletMergeFlag("REQUIRED");
when(orders.selectById(1L)).thenReturn(marked);
var resolved = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("IN-1#2-1"));
assertThat(resolved.inboundCode()).isEqualTo("IN-1");
assertThat(resolved.palletMergeRequired()).isTrue();
assertThat(resolved.palletMergeHint()).contains("合托");
}
@Test @Test
void fbaCartonResolutionRejectsCrossContainerAndInvalidPrefixesAsConflicts() { void fbaCartonResolutionRejectsCrossContainerAndInvalidPrefixesAsConflicts() {
stubCurrent("FBA", "CONT-A"); stubCurrent("FBA", "CONT-A");
@@ -55,6 +55,7 @@ class InboundPlanServiceTest {
ReflectionTestUtils.setField(service, "allocationMapper", allocationMapper); ReflectionTestUtils.setField(service, "allocationMapper", allocationMapper);
ReflectionTestUtils.setField(service, "orderMapper", orderMapper); ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper); ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper);
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of()); org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of()); org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of()); org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of());
@@ -16,6 +16,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -43,7 +44,7 @@ class LocalLooseboardServiceTest {
@Mock LocalLooseboardOperationMapper operations; @Mock LocalLooseboardOperationMapper operations;
@Mock LastlegOrderMapper lastlegOrders; @Mock LastlegOrderMapper lastlegOrders;
@Mock LastlegOrderStatusHistoryMapper lastlegHistories; @Mock LastlegOrderStatusHistoryMapper lastlegHistories;
@Mock LastlegOrderNoGenerator lastlegOrderNoGenerator; @Spy LastlegOrderNoGenerator lastlegOrderNoGenerator = new LastlegOrderNoGenerator();
private LocalLooseboardService service; private LocalLooseboardService service;
private final Map<String, LocalLooseboardOperation> operationRows = new HashMap<>(); private final Map<String, LocalLooseboardOperation> operationRows = new HashMap<>();
@@ -45,6 +45,7 @@ class OverseasProcurementServiceTest {
ReflectionTestUtils.setField(service, "histories", histories); ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory); ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper()); ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
} }
@Test @Test
@@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -44,7 +45,7 @@ class RelabelApplicationServiceTest {
@Mock RelabelStatusHistoryMapper histories; @Mock RelabelStatusHistoryMapper histories;
@Mock LastlegOrderMapper orders; @Mock LastlegOrderMapper orders;
@Mock LastlegOrderStatusHistoryMapper orderHistories; @Mock LastlegOrderStatusHistoryMapper orderHistories;
@Mock LastlegOrderNoGenerator orderNoGenerator; @Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
private RelabelApplicationService service; private RelabelApplicationService service;
@@ -58,7 +59,6 @@ class RelabelApplicationServiceTest {
ReflectionTestUtils.setField(service, "orderHistories", orderHistories); ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator); ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
ReflectionTestUtils.setField(service, "objectMapper", mapper()); ReflectionTestUtils.setField(service, "objectMapper", mapper());
lenient().when(orderNoGenerator.next()).thenReturn("LMO-RELABEL-1");
} }
@Test @Test
@@ -91,10 +91,10 @@ class RelabelApplicationServiceTest {
assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus()); assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus());
assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus()); assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion()); assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion());
assertEquals("LMO-RELABEL-1", tailOrder.getValue().getOrderNo()); assertTrue(tailOrder.getValue().getOrderNo().matches("RL-WH-\\d{8}-\\d{6}"));
assertEquals(tailOrder.getValue().getInboundCode(), tailOrder.getValue().getSourceInboundId()); assertEquals(tailOrder.getValue().getInboundCode(), tailOrder.getValue().getSourceInboundId());
assertEquals(tailOrder.getValue().getInboundCode(), application.getValue().getRelabelNo()); assertEquals(tailOrder.getValue().getInboundCode(), application.getValue().getRelabelNo());
assertTrue(tailOrder.getValue().getInboundCode().startsWith("FBR")); assertTrue(tailOrder.getValue().getInboundCode().startsWith("RL-"));
assertEquals(10, tailOrder.getValue().getPackageCount()); assertEquals(10, tailOrder.getValue().getPackageCount());
assertEquals(6, application.getValue().getNotifiedPackageCount()); assertEquals(6, application.getValue().getNotifiedPackageCount());
assertEquals("SKU-A", relabelItem.getValue().getSkuCode()); assertEquals("SKU-A", relabelItem.getValue().getSkuCode());
@@ -7,13 +7,19 @@ import static org.junit.jupiter.api.Assertions.*;
class LastlegOrderNoGeneratorTest { class LastlegOrderNoGeneratorTest {
@Test void actualContainerNumberIsStableAndShared() { @Test void actualContainerNumberIsStableAndShared() {
LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator(); LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator();
assertEquals("LMO2087379674555461633", generator.forContainerNo("2087379674555461633")); assertTrue(generator.forContainerNo("2087379674555461633").matches("FL-WH-\\d{8}-\\d{6}"));
assertEquals(generator.forContainerNo("MSCU1"), new LastlegOrderNoGenerator().forContainerNo("MSCU1")); assertEquals(generator.forContainerNo("MSCU1"), new LastlegOrderNoGenerator().forContainerNo("MSCU1"));
assertNotEquals(generator.forContainerNo("MSCU1"), generator.forContainerNo("CAB1")); assertNotEquals(generator.forContainerNo("MSCU1"), generator.forContainerNo("CAB1"));
assertTrue(generator.forContainerNo("MSCU1234567890").length() <= 32); assertTrue(generator.forContainerNo("MSCU1234567890").length() <= 32);
assertThrows(IllegalArgumentException.class, () -> generator.forContainerNo(" ")); assertThrows(IllegalArgumentException.class, () -> generator.forContainerNo(" "));
} }
@Test void formatsBusinessWarehouseAndDate() {
String value = new LastlegOrderNoGenerator().next("OUTBOUND", "warehouse-LAX-01", 123L);
assertTrue(value.matches("OB-LAX-\\d{8}-000123"), value);
assertTrue(value.length() <= 24);
}
@Test void remainsUniqueAcrossInstancesAndConcurrentCalls() throws Exception { @Test void remainsUniqueAcrossInstancesAndConcurrentCalls() throws Exception {
LastlegOrderNoGenerator beforeRestart=new LastlegOrderNoGenerator(); LastlegOrderNoGenerator beforeRestart=new LastlegOrderNoGenerator();
String first=beforeRestart.next(); String first=beforeRestart.next();
@@ -6,6 +6,7 @@ import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory; import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton; import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; 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.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
@@ -27,6 +28,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -39,6 +42,7 @@ class LastlegOrderServiceTest {
@Mock LastlegOrderFeeMapper fees; @Mock LastlegOrderFeeMapper fees;
@Mock LastlegOrderOperationLogMapper operations; @Mock LastlegOrderOperationLogMapper operations;
@Mock InboundPlanCartonMapper cartons; @Mock InboundPlanCartonMapper cartons;
@Mock InventoryCommandService inventory;
LastlegOrderService service; LastlegOrderService service;
@BeforeEach @BeforeEach
@@ -50,6 +54,7 @@ class LastlegOrderServiceTest {
ReflectionTestUtils.setField(service, "feeMapper", fees); ReflectionTestUtils.setField(service, "feeMapper", fees);
ReflectionTestUtils.setField(service, "operationLogMapper", operations); ReflectionTestUtils.setField(service, "operationLogMapper", operations);
ReflectionTestUtils.setField(service, "cartonMapper", cartons); ReflectionTestUtils.setField(service, "cartonMapper", cartons);
ReflectionTestUtils.setField(service, "inventory", inventory);
} }
@Test @Test
@@ -115,6 +120,37 @@ class LastlegOrderServiceTest {
verify(operations).insert(any(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class)); verify(operations).insert(any(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class));
} }
@Test
void palletMergeMarkIsPersistedAndAuditedForPdaReminder() {
LastlegOrder order = order(11L, "100", "INBOUND_PLANNED", "IB-1");
when(orders.selectOne(any())).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
when(orders.updatePalletMergeFlag(any(), anyLong(), any(), any())).thenReturn(1);
service.setPalletMergeRequired("100", 11L,
new LastlegOrderRequests.SetPalletMergeRequired(true), "platform_admin");
verify(orders).updatePalletMergeFlag(org.mockito.ArgumentMatchers.eq("100"),
org.mockito.ArgumentMatchers.eq(11L), org.mockito.ArgumentMatchers.eq("REQUIRED"), any());
ArgumentCaptor<com.baiyue.lastleg.entity.LastlegOrderOperationLog> operation =
ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class);
verify(operations).insert(operation.capture());
assertEquals("标记合托", operation.getValue().getOperationNode());
assertTrue(operation.getValue().getResult().contains("PDA"));
}
@Test
void completedPhysicalMergeCannotBeUnmarked() {
LastlegOrder order = order(11L, "100", "INBOUND_PLANNED", "IB-1");
order.setPalletMergeFlag("MERGED");
when(orders.selectOne(any())).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
assertThrows(ResponseStatusException.class, () -> service.setPalletMergeRequired("100", 11L,
new LastlegOrderRequests.SetPalletMergeRequired(false), "platform_admin"));
verify(orders, never()).updatePalletMergeFlag(any(), anyLong(), any(), any());
}
@Test @Test
void transferRejectsAnOrderAlreadyBoundToInboundPlan() { void transferRejectsAnOrderAlreadyBoundToInboundPlan() {
LastlegOrder order = order(11L, "100", "DESTINATION_ASSIGNED", "IB-1"); LastlegOrder order = order(11L, "100", "DESTINATION_ASSIGNED", "IB-1");
@@ -157,6 +193,38 @@ class LastlegOrderServiceTest {
assertEquals(25, page.getValue().getSize()); assertEquals(25, page.getValue().getSize());
} }
@Test
void holdingBeforeInboundPersistsTheOrderMarkerAndOperationRemark() {
LastlegOrder order = order(11L, "100", "PENDING_INBOUND", null);
when(orders.selectOne(any())).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
service.setDetained("100", 11L, new LastlegOrderRequests.SetDetained(true, "等待质检"), "operator");
assertTrue(order.getDetained());
verify(orders).updateById(order);
ArgumentCaptor<com.baiyue.lastleg.entity.LastlegOrderOperationLog> operation =
ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class);
verify(operations).insert(operation.capture());
assertEquals("暂扣", operation.getValue().getOperationNode());
assertEquals("等待质检", operation.getValue().getResult());
verify(inventory, never()).releaseInboundOrderHold(any(), any(), anyLong(), any(), any());
}
@Test
void releasingAfterInboundMovesAllOrderHeldInventoryToAvailable() {
LastlegOrder order = order(11L, "100", "INBOUND_COMPLETED", "IP-1");
order.setOrderNo("LM-1");
order.setDetained(true);
when(orders.selectOne(any())).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
service.setDetained("100", 11L, new LastlegOrderRequests.SetDetained(false, "质检通过"), "operator");
verify(inventory).releaseInboundOrderHold("100", "operator", 11L, "LM-1", "质检通过");
assertEquals(false, order.getDetained());
}
private static LastlegOrder order(long id, String customerId, String status, String inboundOrderNo) { private static LastlegOrder order(long id, String customerId, String status, String inboundOrderNo) {
LastlegOrder order = new LastlegOrder(); LastlegOrder order = new LastlegOrder();
order.setId(id); order.setId(id);
@@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -70,7 +71,7 @@ class WarehouseTransferServiceTest {
@Mock RelabelApplicationMapper relabelApplications; @Mock RelabelApplicationMapper relabelApplications;
@Mock RelabelItemMapper relabelItems; @Mock RelabelItemMapper relabelItems;
@Mock RelabelStatusHistoryMapper relabelHistories; @Mock RelabelStatusHistoryMapper relabelHistories;
@Mock LastlegOrderNoGenerator orderNoGenerator; @Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
@Mock InventoryCommandService inventory; @Mock InventoryCommandService inventory;
@Mock InventoryCommandMapper inventoryCommands; @Mock InventoryCommandMapper inventoryCommands;
private WarehouseTransferService service; private WarehouseTransferService service;
@@ -116,7 +117,7 @@ class WarehouseTransferServiceTest {
assertEquals(WarehouseTransferService.RETURN_RELABEL_ORDER_TYPE, tailOrder.getValue().getOrderType()); assertEquals(WarehouseTransferService.RETURN_RELABEL_ORDER_TYPE, tailOrder.getValue().getOrderType());
assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus()); assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus());
assertEquals("WAREHOUSE_TRANSFER:PENDING_REVIEW;RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus()); assertEquals("WAREHOUSE_TRANSFER:PENDING_REVIEW;RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
assertEquals("LMO-TRANSFER-1", tailOrder.getValue().getOrderNo()); assertTrue(tailOrder.getValue().getOrderNo().matches("WT-WH-\\d{8}-\\d{6}"));
assertNull(tailOrder.getValue().getTransferWarehouseId()); assertNull(tailOrder.getValue().getTransferWarehouseId());
verify(sourceOrders).insert(any(WarehouseTransferSourceOrder.class)); verify(sourceOrders).insert(any(WarehouseTransferSourceOrder.class));
verify(relabelApplications).insert(any(RelabelApplication.class)); verify(relabelApplications).insert(any(RelabelApplication.class));