暂扣 标记合托功能

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)));
}
@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")
@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)
@@ -53,6 +53,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import java.math.BigDecimal;
import java.util.*;
@@ -77,6 +78,7 @@ public class DeliveryTransferService {
@Resource private LastlegOrderMapper tailOrders;
@Resource private LastlegOrderStatusHistoryMapper histories;
@Resource private LastlegOrderDetailMapper tailOrderDetails;
@Resource private LastlegOrderNoGenerator orderNoGenerator;
@Transactional
public DeliveryResponses.TransferResult transfer(String customerId, String dispatchNo, String operatorId,
@@ -105,7 +107,7 @@ public class DeliveryTransferService {
LocalDateTime now = LocalDateTime.now();
OutboundOrder outbound = new OutboundOrder();
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.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());
@@ -141,7 +143,7 @@ public class DeliveryTransferService {
DeliveryOrder delivery = new DeliveryOrder();
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.setDispatchOrderId(dispatch.getId()); delivery.setDispatchNo(dispatch.getDispatchNo());
delivery.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); delivery.setOverseasWarehouseName(dispatch.getOverseasWarehouseName());
@@ -197,14 +199,16 @@ public class DeliveryTransferService {
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (existing != null) return taskResult(existing, pdaTasks.lockByOutbound(customerId, existing.getId()));
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();
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.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);
outboundOrders.insert(outbound);
String selectedWarehouseId = null;
for (DeliveryRequests.OneClickItem input : request.items()) {
InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId());
if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found");
@@ -44,6 +44,7 @@ import java.util.Set;
@Service
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 PENDING_REVIEW = "PENDING_REVIEW";
public static final String APPROVED = "APPROVED";
@@ -105,7 +106,7 @@ public class CargoDiscardService {
LocalDateTime now = LocalDateTime.now();
CargoDiscardApplication application = new CargoDiscardApplication();
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.setInboundCodes(orderCache.values().stream().map(LastlegOrder::getInboundCode)
.distinct().sorted().collect(java.util.stream.Collectors.joining(",")));
@@ -31,6 +31,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@@ -48,6 +49,7 @@ public class DispatchService {
@Resource private DispatchDraftMapper drafts;
@Resource private DispatchDraftContainerMapper draftContainers;
@Resource private DispatchCandidateMapper candidates;
@Resource private LastlegOrderNoGenerator orderNoGenerator;
public String resolveCustomerScopeForPlans(List<Long> planIds) {
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
@@ -239,7 +241,7 @@ public class DispatchService {
DispatchOrder dispatch = new DispatchOrder();
dispatch.setId(IdWorker.getId());
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.setOverseasWarehouseName(selected.get(0).overseasWarehouseName());
dispatch.setRoute(StringUtils.hasText(request.route()) ? request.route().trim() : route(selected));
@@ -68,4 +68,15 @@ public final class LastlegOrderRequests {
@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
public class ExceptionOrderService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
@Resource private ExceptionOrderMapper exceptionOrders;
@Resource private LastlegOrderMapper lastMileOrders;
@Resource private LastlegInboundOrderMapper inboundOrders;
@@ -158,7 +159,7 @@ public class ExceptionOrderService {
LocalDateTime now = LocalDateTime.now();
ExceptionOrder value = new ExceptionOrder();
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.setLastMileOrderId(relation.lastMileOrderId()); value.setLastMileOrderNo(relation.lastMileOrderNo());
value.setInboundOrderId(relation.inboundOrderId()); value.setInboundCode(relation.inboundCode());
@@ -26,14 +26,28 @@ public final class ReceivingContainerResponses {
String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName,
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,
String destinationType, String overseasWarehouseId,
String overseasWarehouseName, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName,
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,
String fbaWarehouseCode, String inboundCode,
@@ -41,7 +55,15 @@ public final class ReceivingContainerResponses {
public record Receipt(String result, String containerNo, String inboundCode,
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) { }
}
@@ -224,15 +224,17 @@ public class InboundStockReceiptService {
String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId() + ":" + sku.skuCode();
String overseasWarehouseId = required(order.getOverseasWarehouseId(),
"Actual overseas warehouse is required for stock-in");
InventoryCommandResponse result = inventory.stockIn(customerId, task.getOperatorId(),
new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId,
InventoryCommandRequests.StockIn stockIn = new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId,
null, order.getOverseasWarehouseName(), line.getDestinationWarehouseId(), destinationCode,
line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(),
sku.skuCode(), sku.productNameZh(), sku.productNameEn(),
pallet.getWarehousePalletId(), pallet.getPalletNo(), plan.getPlanNo(),
pallet.getWarehouseLocationId(), null, packages, order.getOrderNo(),
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();
receipt.setId(IdWorker.getId());
@@ -61,6 +61,7 @@ import java.util.stream.Collectors;
public class ReceivingContainerService {
private static final String PHOTO_DOWNLOAD_PATH =
"/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 InboundPlanLineMapper lines;
@@ -245,7 +246,8 @@ public class ReceivingContainerService {
inbound.inboundCode(), inbound.destinationType(), inbound.overseasWarehouseId(),
inbound.overseasWarehouseName(), inbound.destinationWarehouseId(),
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,
@@ -290,7 +292,8 @@ public class ReceivingContainerService {
int total = fbaPlanRefs.stream().mapToInt(refValue -> value(refValue.line().getRequiredPackageCount())).sum();
ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED",
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);
return result;
} catch (RuntimeException failure) {
@@ -328,7 +331,8 @@ public class ReceivingContainerService {
ReceivingContainerResponses.Inbound inbound = inbound(ref);
ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED",
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);
return result;
} catch (RuntimeException failure) {
@@ -539,7 +543,17 @@ public class ReceivingContainerService {
: String.valueOf(ref.plan().getOverseasWarehouseId()), ref.order().getOverseasWarehouseName(),
ref.line().getDestinationWarehouseId(), ref.line().getDestinationWarehouseCode(),
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) {
@@ -36,6 +36,7 @@ import java.util.stream.Collectors;
@Service
public class InboundPlanService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
@Resource private InboundPlanMapper planMapper;
@Resource private InboundPlanLineMapper lineMapper;
@Resource private InboundPlanPalletMapper palletMapper;
@@ -106,7 +107,7 @@ public class InboundPlanService {
LocalDateTime now = LocalDateTime.now();
InboundPlan plan = new InboundPlan();
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.setSourceOrderId(selectedOrderId);
plan.setContainerNo(containerNo);
@@ -31,4 +31,10 @@ public interface InventoryCommandMapper {
List<InventoryBalance> lockInboundTransitBalances(@Param("customerId") String customerId,
@Param("inboundOrderId") Long inboundOrderId,
@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
public InventoryCommandResponse stockIn(String customerId, String operatorId,
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 -> {
List<InventoryBalance> transitBalances = commands.lockInboundTransitBalances(customerId,
request.inboundOrderId(), request.inboundCode());
InventoryBalance balance = balance(customerId, request);
InventoryPosition position = position(customerId, balance, request);
long before = value(position.getAvailablePackages());
position.setAvailablePackages(before + request.packages());
long before = held ? value(position.getHoldPackages()) : value(position.getAvailablePackages());
if (held) position.setHoldPackages(before + request.packages());
else position.setAvailablePackages(before + request.packages());
position.setStatus("OCCUPIED");
position.setVersion(value(position.getVersion()) + 1);
position.setUpdatedAt(LocalDateTime.now());
@@ -74,11 +86,12 @@ public class InventoryCommandService {
long transferred = transferInTransit(customerId, operatorId, operation.getId(), balance,
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);
balances.updateById(balance);
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(),
request.businessOrderNo(), request.inboundOrderNo(), null, request.sourceEventId(),
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,
InventoryBalance target, List<InventoryBalance> sources,
InventoryCommandRequests.StockIn request) {
@@ -70,7 +70,7 @@ public class LocalLooseboardService {
LocalLooseboardOrder order = new LocalLooseboardOrder();
order.setId(IdWorker.getId());
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(),
request.originAddress(), request.destination(), request.destinationAddress(), request.containerType(),
request.packageCount(), request.totalWeightKg(), request.totalVolumeCbm(), request.budgetAmount(),
@@ -257,7 +257,7 @@ public class LocalLooseboardService {
private LastlegOrder createLastlegOrder(LocalLooseboardOrder source, String requestHash, LocalDateTime now) {
LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId());
order.setOrderNo(lastlegOrderNoGenerator.next());
order.setOrderNo(lastlegOrderNoGenerator.next("LOOSEBOARD", source.getDestination(), order.getId()));
order.setCustomerId(source.getCustomerId());
order.setOrderType("LOCAL_LOOSEBOARD");
order.setInboundCode(source.getOrderNo());
@@ -42,4 +42,10 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
int claimReceivingWarehouse(@Param("customerId") String customerId, @Param("orderId") long orderId,
@Param("warehouseId") String warehouseId,
@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
public class OverseasProcurementService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
public static final String REVIEW_PERMISSION = OverseasPermissions.PURCHASE_REVIEW;
public static final String MANAGE_PERMISSION = OverseasPermissions.PURCHASE_EDIT;
public static final String PENDING_REVIEW = "PENDING_REVIEW";
@@ -62,7 +63,7 @@ public class OverseasProcurementService {
order.setId(IdWorker.getId());
order.setCustomerId(customerId);
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.setProductName(request.productName().trim());
order.setProductSpecification(trim(request.productSpecification()));
@@ -67,10 +67,10 @@ public class RelabelApplicationService {
}
LocalDateTime now = LocalDateTime.now();
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();
order.setId(IdWorker.getId());
order.setOrderNo(orderNoGenerator.next());
order.setOrderNo(orderNoGenerator.next("RELABEL", request.overseasWarehouseId(), order.getId()));
order.setCustomerId(customerId);
order.setOrderType("RELABEL_APPLICATION");
order.setInboundCode(relabelNo);
@@ -3,15 +3,59 @@ package com.baiyue.lastleg.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
@Component
public class LastlegOrderNoGenerator {
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. */
@@ -20,14 +64,11 @@ public class LastlegOrderNoGenerator {
throw new IllegalArgumentException("containerNo is required");
}
String normalized = containerNo.trim();
if (normalized.matches("[0-9]{1,29}")) {
return "LMO" + normalized;
}
try {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(normalized.getBytes(StandardCharsets.UTF_8));
String digits = new BigInteger(1, digest).toString();
return "LMO" + digits.substring(0, Math.min(29, digits.length()));
long suffix = java.nio.ByteBuffer.wrap(digest, 0, 8).getLong();
return next("FIRST_LEG", null, suffix);
} catch (NoSuchAlgorithmException 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.inboundplan.entity.InboundPlanCarton;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
@@ -38,6 +39,8 @@ public class LastlegOrderService {
public static final String PENDING_ASSIGNMENT = "PENDING_ASSIGNMENT";
public static final String DESTINATION_ASSIGNED = "DESTINATION_ASSIGNED";
public static final String INBOUND_PLANNED = "INBOUND_PLANNED";
public static final String PALLET_MERGE_REQUIRED = "REQUIRED";
public static final String PALLET_MERGED = "MERGED";
@Resource private LastlegOrderMapper orderMapper;
@Resource private LastlegOrderDetailMapper detailMapper;
@@ -45,6 +48,7 @@ public class LastlegOrderService {
@Resource private LastlegOrderFeeMapper feeMapper;
@Resource private LastlegOrderOperationLogMapper operationLogMapper;
@Resource private InboundPlanCartonMapper cartonMapper;
@Resource private InventoryCommandService inventory;
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
Page<LastlegOrder> page = orderMapper.selectPage(
@@ -110,6 +114,70 @@ public class LastlegOrderService {
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
public int transferDestination(String customerId, LastlegOrderRequests.TransferDestination request) {
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
@@ -83,8 +83,8 @@ public class LoadingCompletedConsumer {
shippingGates.initialize(payload.containerBookingId(), envelope.tenantId(), "",
payload.containerNo(), 0, envelope.handoverVersion());
String orderNo = orderNoGenerator.forContainerNo(payload.containerNo());
JsonNode rawOrders = envelope.payload().path("orders");
String orderNo = orderNoGenerator.forContainerNo(payload.containerNo());
for (int index = 0; index < payload.orders().size(); index++) {
var source = payload.orders().get(index);
JsonNode rawSource = rawOrders.isArray() && index < rawOrders.size() ? rawOrders.get(index) : objectMapper.createObjectNode();
@@ -117,7 +117,7 @@ public class WarehouseTransferService {
LocalDateTime now = LocalDateTime.now();
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);
WarehouseTransferOrder transfer = new WarehouseTransferOrder();
@@ -464,7 +464,7 @@ public class WarehouseTransferService {
private LastlegOrder createTailOrder(String customerId, Long inboundOrderId, String transferNo,
WarehouseTransferRequests.Create request, String requestHash, LocalDateTime now) {
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.setInboundCode(request.inboundCode().trim()); order.setSourceRegion("CUSTOMER");
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,
WarehouseTransferRequests.Create request, LocalDateTime now) {
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();
application.setId(applicationId); application.setCustomerId(customerId); application.setRelabelNo(relabelNo);
application.setLastlegOrderId(order.getId()); application.setCustomerOrderNo(transfer.getTransferNo());
@@ -39,6 +39,7 @@ import java.util.function.Consumer;
@Service
public class UsTruckOrderService {
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
public static final String MANAGE_PERMISSION = OverseasPermissions.TRUCK_STATUS;
public static final String PENDING_REVIEW = "PENDING_REVIEW";
/** Kept for source compatibility; new orders use PENDING_REVIEW. */
@@ -79,7 +80,7 @@ public class UsTruckOrderService {
UsTruckOrder order = new UsTruckOrder();
order.setId(IdWorker.getId());
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.setOrigin(trim(request.origin())); order.setOriginAddress(trim(request.originAddress()));
order.setDestination(trim(request.destination())); order.setDestinationAddress(trim(request.destinationAddress()));