Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
+21
@@ -70,6 +70,27 @@ public class DeliveryTransferController {
|
||||
return ApiResponse.success(service.createReturnOutbound(requiredCustomer(user, customerId), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/outbound-orders/warehouse-transfer/candidates")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_CREATE + "')")
|
||||
@Operation(summary = "List warehouse-transfer pallets that can create an outbound order")
|
||||
public ApiResponse<java.util.List<DeliveryResponses.WarehouseTransferOutboundCandidate>> warehouseTransferCandidates(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@RequestParam(required = false) String warehouseId) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.warehouseTransferOutboundCandidates(customerId, warehouseId));
|
||||
}
|
||||
|
||||
@PostMapping("/outbound-orders/warehouse-transfer")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_CREATE + "')")
|
||||
@Operation(summary = "Create a warehouse-transfer outbound, delivery order and PDA task")
|
||||
public ApiResponse<DeliveryResponses.OutboundDetail> createWarehouseTransferOutbound(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
|
||||
@Valid @RequestBody DeliveryRequests.WarehouseTransferOutbound request) {
|
||||
return ApiResponse.success(service.createWarehouseTransferOutbound(
|
||||
requiredCustomer(user, customerId), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/outbound-orders")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_LIST + "')")
|
||||
public ApiResponse<PageResult<DeliveryResponses.OutboundRow>> outboundPage(
|
||||
|
||||
@@ -75,6 +75,20 @@ public final class DeliveryRequests {
|
||||
@NotNull @Size(min=1, max=200) List<@NotBlank @Size(max=128) String> cartonMarks,
|
||||
@Size(max=1000) String remark,
|
||||
@NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
public record WarehouseTransferOutbound(
|
||||
@NotBlank @Size(max=64) String transferNo,
|
||||
@NotNull @Size(min=1, max=100) List<@NotNull Long> palletIds,
|
||||
@NotBlank @jakarta.validation.constraints.Pattern(regexp = "FBA|PRIVATE") String destinationType,
|
||||
@Size(max=128) String destinationWarehouseId,
|
||||
@Size(max=128) String destinationWarehouseCode,
|
||||
@Size(max=255) String destinationWarehouseName,
|
||||
@Size(max=1000) String deliveryAddress,
|
||||
@Size(max=128) String contactName,
|
||||
@Size(max=64) String contactPhone,
|
||||
@Size(max=32) String deliveryMethod,
|
||||
LocalDateTime plannedDeliveryAt,
|
||||
@Size(max=1000) String remark,
|
||||
@NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
public record OneClickItem(@NotNull Long inventoryPositionId, @NotNull @Min(1) Long packages,
|
||||
@NotBlank @Size(max=128) String sku,
|
||||
@Size(max=200) List<@NotBlank @Size(max=128) String> cartonMarks) {
|
||||
|
||||
+12
-1
@@ -31,7 +31,8 @@ public final class DeliveryResponses {
|
||||
String remark, LocalDateTime createdAt, long version, List<String> allowedActions,
|
||||
String bolNo, DeliveryDocumentDtos.Summary documentSummary,
|
||||
String destinationWarehouseId, String destinationWarehouseCode,
|
||||
String destinationWarehouseName) {}
|
||||
String destinationWarehouseName, String destinationType,
|
||||
String deliveryAddress, String contactName, String contactPhone, String postalCode) {}
|
||||
|
||||
public record DeliverySku(String inboundCode, String customerName, String destinationWarehouseCode,
|
||||
String receivingAddress, String shipmentNo, String fbaTrackingNo,
|
||||
@@ -59,6 +60,16 @@ public final class DeliveryResponses {
|
||||
String cartonMark, String fbaCartonMark,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
long totalQuantity, String status) {}
|
||||
public record WarehouseTransferOutboundCandidate(String customerId, String customerName,
|
||||
String transferNo, String overseasWarehouseId,
|
||||
String transferStatus, boolean needRelabel,
|
||||
String relabelNo, String destinationType,
|
||||
String destinationWarehouseCode,
|
||||
String privateDestinationAddress,
|
||||
String privateDestinationPostalCode,
|
||||
Long palletId, Long warehousePalletId, String palletNo,
|
||||
int packageCount, BigDecimal grossWeightKg,
|
||||
BigDecimal volumeCbm) {}
|
||||
public record PdaTask(Long id, String taskNo, String status, String outboundNo, LocalDateTime pushedAt, LocalDateTime completedAt) {}
|
||||
public record PdaTaskResolution(String workflow, String taskNo, String outboundNo, String dispatchNo) {}
|
||||
public record PdaPickPosition(Long inventoryPositionId, String palletNo, String cartonMark,
|
||||
|
||||
@@ -26,6 +26,11 @@ public class DeliveryOrder {
|
||||
private String destinationWarehouseId;
|
||||
private String destinationWarehouseCode;
|
||||
private String destinationWarehouseName;
|
||||
private String destinationType;
|
||||
private String deliveryAddress;
|
||||
private String postalCode;
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private String deliveryGroupKey;
|
||||
private String status;
|
||||
private String waybillNo;
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ public class DeliveryCompletedOutboxService {
|
||||
Objects.requireNonNull(completedAt, "completedAt");
|
||||
Map<String, LastlegOrder> bySourceInbound = new LinkedHashMap<>();
|
||||
for (LastlegOrder order : orders == null ? List.<LastlegOrder>of() : orders) {
|
||||
if ("RELABEL_APPLICATION".equals(order.getOrderType())) continue;
|
||||
if (!StringUtils.hasText(order.getSourceInboundId())) continue;
|
||||
requireText(order.getSourceOrderId(), "sourceOrderId");
|
||||
requireText(order.getInboundCode(), "inboundCode");
|
||||
|
||||
+240
-4
@@ -60,7 +60,9 @@ import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -97,6 +99,7 @@ public class DeliveryTransferService {
|
||||
@Resource private InventoryBalanceMapper inventoryBalances;
|
||||
@Resource private InventoryCommandService inventoryCommands;
|
||||
@Resource private WarehouseTransferPalletMapper warehouseTransferPallets;
|
||||
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
|
||||
@Resource private DropshipOrderProfileMapper dropshipProfiles;
|
||||
@Resource private DropshipCartonMapper dropshipCartons;
|
||||
@Resource private DropshipCartonSkuMapper dropshipCartonSkus;
|
||||
@@ -217,6 +220,11 @@ public class DeliveryTransferService {
|
||||
firstText(destination.getDestinationWarehouseCode(), destination.getFbaWarehouseCode()),
|
||||
destination.getDestinationWarehouseId()));
|
||||
delivery.setDestinationWarehouseName(trim(destination.getDestinationWarehouseName()));
|
||||
delivery.setDestinationType(destination.getDestinationType());
|
||||
delivery.setDeliveryAddress(destination.getDeliveryAddress());
|
||||
delivery.setPostalCode(destination.getPostalCode());
|
||||
delivery.setContactName(destination.getContactName());
|
||||
delivery.setContactPhone(destination.getContactPhone());
|
||||
delivery.setDeliveryGroupKey(entry.getKey());
|
||||
LastlegOrder firstOrder = firstTailOrder(customerId, groupItems);
|
||||
delivery.setCustomerName(firstOrder == null ? null : firstOrder.getCustomerName()); delivery.setStatus("WAIT_DELIVERY"); delivery.setVersion(0L);
|
||||
@@ -395,6 +403,155 @@ public class DeliveryTransferService {
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<DeliveryResponses.WarehouseTransferOutboundCandidate> warehouseTransferOutboundCandidates(
|
||||
String customerId, String warehouseId) {
|
||||
List<WarehouseTransferOrder> transfers = warehouseTransfers.selectList(
|
||||
new LambdaQueryWrapper<WarehouseTransferOrder>()
|
||||
.eq(StringUtils.hasText(customerId), WarehouseTransferOrder::getCustomerId, trim(customerId))
|
||||
.eq(StringUtils.hasText(warehouseId), WarehouseTransferOrder::getOperatingWarehouseId, trim(warehouseId))
|
||||
.eq(WarehouseTransferOrder::getStatus, "WAIT_FBA_DESTINATION")
|
||||
.orderByDesc(WarehouseTransferOrder::getUpdatedAt));
|
||||
List<DeliveryResponses.WarehouseTransferOutboundCandidate> result = new ArrayList<>();
|
||||
for (WarehouseTransferOrder transfer : transfers) {
|
||||
LastlegOrder order = tailOrders.selectById(transfer.getLastlegOrderId());
|
||||
List<WarehouseTransferPallet> completedPallets = warehouseTransferPallets.listOrder(
|
||||
transfer.getCustomerId(), transfer.getId()).stream()
|
||||
.filter(pallet -> "COMPLETED".equals(pallet.getStatus()) && pallet.getWarehousePalletId() != null)
|
||||
.toList();
|
||||
long available = 0;
|
||||
for (WarehouseTransferPallet pallet : completedPallets) {
|
||||
long palletAvailable = inventoryPositions.listPallet(transfer.getCustomerId(), pallet.getWarehousePalletId())
|
||||
.stream().filter(position -> transfer.getOperatingWarehouseId().equals(position.getOverseasWarehouseId()))
|
||||
.mapToLong(position -> value(position.getAvailablePackages())).sum();
|
||||
available += palletAvailable;
|
||||
}
|
||||
if (available <= 0 || completedPallets.isEmpty()) continue;
|
||||
result.add(new DeliveryResponses.WarehouseTransferOutboundCandidate(
|
||||
transfer.getCustomerId(), order == null ? null : order.getCustomerName(), transfer.getTransferNo(),
|
||||
transfer.getOperatingWarehouseId(), transfer.getStatus(), Boolean.TRUE.equals(transfer.getNeedRelabel()),
|
||||
transfer.getRelabelNo(), transfer.getDestinationType(), transfer.getDestinationWarehouseCode(),
|
||||
transfer.getPrivateDestinationAddress(), transfer.getPrivateDestinationPostalCode(),
|
||||
null, null, null, Math.toIntExact(available),
|
||||
completedPallets.stream().map(WarehouseTransferPallet::getGrossWeightKg).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add),
|
||||
completedPallets.stream().map(WarehouseTransferPallet::getVolumeCbm).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.OutboundDetail createWarehouseTransferOutbound(String customerId, String operatorId,
|
||||
DeliveryRequests.WarehouseTransferOutbound request) {
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
OutboundOrder replay = outboundOrders.findByIdempotency(customerId, key);
|
||||
if (replay != null) {
|
||||
if (!"WAREHOUSE_TRANSFER_OUTBOUND".equals(replay.getOutboundType())
|
||||
|| !requestHash.equals(replay.getRequestHash())) {
|
||||
throw conflict("相同幂等键不能用于不同的仓储转运出库请求");
|
||||
}
|
||||
return outboundDetail(replay.getOutboundNo());
|
||||
}
|
||||
WarehouseTransferOrder transfer = warehouseTransfers.lockNo(customerId, request.transferNo().trim());
|
||||
if (transfer == null) throw notFound("仓储转运单不存在");
|
||||
if (!Set.of("READY_FOR_DISPATCH", "WAIT_FBA_DESTINATION").contains(transfer.getStatus())) {
|
||||
throw conflict("仓储转运单尚未完成换标或已经生成出库单");
|
||||
}
|
||||
String destinationType = request.destinationType().trim().toUpperCase(Locale.ROOT);
|
||||
if ("FBA".equals(destinationType) && !StringUtils.hasText(request.destinationWarehouseCode())) {
|
||||
throw conflict("FBA出库必须选择目的仓");
|
||||
}
|
||||
if ("PRIVATE".equals(destinationType) && (!StringUtils.hasText(request.deliveryAddress())
|
||||
|| !StringUtils.hasText(request.contactName()) || !StringUtils.hasText(request.contactPhone()))) {
|
||||
throw conflict("私人仓出库必须填写地址、联系人和联系电话");
|
||||
}
|
||||
List<Long> palletIds = request.palletIds().stream().distinct().toList();
|
||||
if (palletIds.size() != request.palletIds().size()) throw conflict("出库托盘不可重复");
|
||||
List<WarehouseTransferPallet> selectedPallets = new ArrayList<>();
|
||||
for (Long palletId : palletIds) {
|
||||
WarehouseTransferPallet pallet = warehouseTransferPallets.lockOne(customerId, transfer.getId(), palletId);
|
||||
if (pallet == null || !"COMPLETED".equals(pallet.getStatus()) || pallet.getWarehousePalletId() == null) {
|
||||
throw conflict("所选托盘不属于该仓储转运单或尚未完成入库:" + palletId);
|
||||
}
|
||||
selectedPallets.add(pallet);
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("OUTBOUND", transfer.getOperatingWarehouseId(), outbound.getId()));
|
||||
outbound.setOverseasWarehouseId(transfer.getOperatingWarehouseId());
|
||||
outbound.setOutboundType("WAREHOUSE_TRANSFER_OUTBOUND"); outbound.setStatus("WAIT_PICK");
|
||||
outbound.setRecipientInfo("PRIVATE".equals(destinationType)
|
||||
? request.contactName().trim() + " / " + request.contactPhone().trim() + " / " + request.deliveryAddress().trim()
|
||||
: request.destinationWarehouseCode().trim());
|
||||
outbound.setPalletCount(selectedPallets.size()); outbound.setIdempotencyKey(key);
|
||||
outbound.setRequestHash(requestHash); outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now);
|
||||
outboundOrders.insert(outbound);
|
||||
|
||||
int packageCount = 0;
|
||||
for (WarehouseTransferPallet pallet : selectedPallets) {
|
||||
List<InventoryPosition> positions = inventoryPositions.listPallet(customerId, pallet.getWarehousePalletId())
|
||||
.stream().filter(position -> transfer.getOperatingWarehouseId().equals(position.getOverseasWarehouseId()))
|
||||
.filter(position -> value(position.getAvailablePackages()) > 0).toList();
|
||||
if (positions.isEmpty()) throw conflict("所选托盘没有可出库库存:" + pallet.getPalletNo());
|
||||
for (InventoryPosition position : positions) {
|
||||
InventoryBalance balance = inventoryBalances.selectById(position.getBalanceId());
|
||||
if (balance == null || !customerId.equals(balance.getCustomerId())) throw conflict("托盘库存余额不存在");
|
||||
int packages = Math.toIntExact(value(position.getAvailablePackages()));
|
||||
OutboundPalletItem item = new OutboundPalletItem();
|
||||
item.setId(IdWorker.getId()); item.setCustomerId(customerId); item.setOutboundOrderId(outbound.getId());
|
||||
item.setWarehousePalletId(pallet.getWarehousePalletId()); item.setPalletNo(pallet.getPalletNo());
|
||||
item.setDestinationWarehouseId(trim(request.destinationWarehouseId()));
|
||||
item.setDestinationWarehouseCode(trim(request.destinationWarehouseCode()));
|
||||
item.setDestinationWarehouseName(trim(request.destinationWarehouseName()));
|
||||
item.setPackageCount(packages); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setSkuCode(balance.getSkuCode()); item.setInboundCode(balance.getInboundCode());
|
||||
item.setOrderRefs(transfer.getTransferNo()); item.setCreatedAt(now); outboundPallets.insert(item);
|
||||
packageCount = Math.addExact(packageCount, packages);
|
||||
}
|
||||
}
|
||||
outbound.setPackageCount(packageCount); outboundOrders.updateById(outbound);
|
||||
|
||||
LastlegOrder tailOrder = tailOrders.selectById(transfer.getLastlegOrderId());
|
||||
DeliveryOrder delivery = new DeliveryOrder();
|
||||
delivery.setId(IdWorker.getId()); delivery.setCustomerId(customerId);
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", transfer.getOperatingWarehouseId(), delivery.getId()));
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(transfer.getOperatingWarehouseId());
|
||||
delivery.setCustomerName(tailOrder == null ? null : tailOrder.getCustomerName());
|
||||
delivery.setDestinationType(destinationType); delivery.setDestinationWarehouseId(trim(request.destinationWarehouseId()));
|
||||
delivery.setDestinationWarehouseCode(trim(request.destinationWarehouseCode()));
|
||||
delivery.setDestinationWarehouseName(trim(request.destinationWarehouseName()));
|
||||
delivery.setDeliveryAddress(trim(request.deliveryAddress())); delivery.setContactName(trim(request.contactName()));
|
||||
delivery.setContactPhone(trim(request.contactPhone())); delivery.setDeliveryGroupKey(destinationType + ":" +
|
||||
firstText(request.destinationWarehouseCode(), request.deliveryAddress()));
|
||||
delivery.setStatus("WAIT_DELIVERY"); delivery.setDeliveryMethod(firstText(request.deliveryMethod(), "TRUCK"));
|
||||
delivery.setPackageCount(packageCount); delivery.setPalletCount(selectedPallets.size());
|
||||
delivery.setTotalWeightKg(selectedPallets.stream().map(WarehouseTransferPallet::getGrossWeightKg)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
delivery.setTotalVolumeCbm(selectedPallets.stream().map(WarehouseTransferPallet::getVolumeCbm)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
delivery.setPlannedDeliveryAt(request.plannedDeliveryAt()); delivery.setRemark(trim(request.remark()));
|
||||
delivery.setIdempotencyKey(childKey(key, "DELIVERY")); delivery.setCreatedBy(operatorId); delivery.setVersion(0L);
|
||||
delivery.setCreatedAt(now); delivery.setUpdatedAt(now); deliveryOrders.insert(delivery);
|
||||
recordOperation(customerId, delivery.getId(), operatorId, "CREATE_DELIVERY", "仓储转运出库自动生成派送单", now);
|
||||
|
||||
OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(customerId);
|
||||
task.setOutboundOrderId(outbound.getId()); task.setTaskNo("PDA" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + task.getId());
|
||||
task.setStatus("PENDING"); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); pdaTasks.insert(task);
|
||||
transfer.setStatus("WAIT_OUTBOUND");
|
||||
if ("FBA".equals(destinationType)) transfer.setFbaWarehouseCode(request.destinationWarehouseCode().trim());
|
||||
transfer.setUpdatedAt(now); warehouseTransfers.updateById(transfer);
|
||||
if (tailOrder != null && customerId.equals(tailOrder.getCustomerId())) {
|
||||
tailOrder.setCurrentStatus("WAIT_OUTBOUND"); tailOrder.setOutboundOrderNo(outbound.getOutboundNo());
|
||||
tailOrder.setDeliveryOrderNo(delivery.getDeliveryNo()); tailOrder.setFbaWarehouseCode(trim(request.destinationWarehouseCode()));
|
||||
tailOrder.setSubOrderStatus("WAREHOUSE_TRANSFER:WAIT_OUTBOUND"); tailOrder.setUpdatedAt(now); tailOrders.updateById(tailOrder);
|
||||
}
|
||||
return outboundDetail(outbound.getOutboundNo());
|
||||
}
|
||||
|
||||
private String validateOneClickItems(String customerId, List<DeliveryRequests.OneClickItem> items) {
|
||||
String warehouseId = null;
|
||||
Map<String, Long> requestedPackages = new LinkedHashMap<>();
|
||||
@@ -541,6 +698,40 @@ public class DeliveryTransferService {
|
||||
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
if ("WAREHOUSE_TRANSFER_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
if (request.inventoryPositionId() == null) throw conflict("仓储转运出库必须确认实际库存位置");
|
||||
InventoryPosition selectedPosition = inventoryPositions.selectById(request.inventoryPositionId());
|
||||
if (selectedPosition == null || !customerId.equals(selectedPosition.getCustomerId())
|
||||
|| !warehouseId.equals(selectedPosition.getOverseasWarehouseId())
|
||||
|| !Objects.equals(selectedPosition.getPalletId(), item.getWarehousePalletId())) {
|
||||
throw conflict("确认的库存位置不属于该仓储转运出库托盘");
|
||||
}
|
||||
InventoryBalance balance = inventoryBalances.selectById(selectedPosition.getBalanceId());
|
||||
if (!matchesOutboundItem(customerId, warehouseId, item, balance)) {
|
||||
throw conflict("确认的库存SKU或入仓码与出库明细不一致");
|
||||
}
|
||||
if (request.packages() > value(item.getPackageCount()) - value(item.getPickedPackageCount())) {
|
||||
throw conflict("出库数量超过仓储转运出库明细余量");
|
||||
}
|
||||
inventoryCommands.allocate(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
selectedPosition.getId(), request.packages(), item.getOrderRefs(), outbound.getOutboundNo(), null,
|
||||
request.idempotencyKey() + ":ALLOCATE", "仓储转运出库分配"));
|
||||
inventoryCommands.pick(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
selectedPosition.getId(), request.packages(), item.getOrderRefs(), outbound.getOutboundNo(), null,
|
||||
request.idempotencyKey() + ":PICK", "仓储转运出库拣货"));
|
||||
inventoryCommands.ship(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
selectedPosition.getId(), request.packages(), item.getOrderRefs(), outbound.getOutboundNo(), null,
|
||||
request.idempotencyKey() + ":SHIP", "仓储转运出库"));
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId);
|
||||
pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId());
|
||||
pick.setInventoryPositionId(selectedPosition.getId()); pick.setPackages(request.packages());
|
||||
pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
item.setPickedPackageCount(value(item.getPickedPackageCount()) + request.packages().intValue());
|
||||
item.setStatus(item.getPickedPackageCount() >= item.getPackageCount() ? "PICKED" : "PARTIAL_PICKED");
|
||||
outboundPallets.updateById(item); updateOutboundPickingStatus(customerId, outbound);
|
||||
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
InventoryPosition selectedPosition = inventoryPositions.selectById(request.inventoryPositionId());
|
||||
if (selectedPosition == null || !customerId.equals(selectedPosition.getCustomerId()) || !Objects.equals(selectedPosition.getPalletId(), item.getWarehousePalletId())) throw conflict("Scanned inventory position does not belong to the outbound pallet");
|
||||
if (!warehouseId.equals(selectedPosition.getOverseasWarehouseId())) {
|
||||
@@ -612,8 +803,8 @@ public class DeliveryTransferService {
|
||||
if (outbound == null || !boundWarehouseId.equals(outbound.getOverseasWarehouseId())) {
|
||||
throw notFound("Outbound order not found in the bound warehouse");
|
||||
}
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(outbound.getOutboundType())) {
|
||||
throw conflict("整托扫码仅适用于一件代发出库");
|
||||
if (!Set.of("ONE_CLICK_DROPSHIP", "RELABEL_OUTBOUND", "WAREHOUSE_TRANSFER_OUTBOUND").contains(outbound.getOutboundType())) {
|
||||
throw conflict("整托扫码仅适用于一件代发或换标出库");
|
||||
}
|
||||
if (!Set.of("PENDING", "IN_PROGRESS").contains(task.getStatus())) {
|
||||
throw conflict("PDA task is not available for picking");
|
||||
@@ -695,6 +886,23 @@ public class DeliveryTransferService {
|
||||
new RelabelRequests.ChangeStatus("COMPLETED", "换标出库PDA作业完成"));
|
||||
}
|
||||
}
|
||||
if ("WAREHOUSE_TRANSFER_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
List<String> transferNos = items.stream().map(OutboundPalletItem::getOrderRefs)
|
||||
.filter(StringUtils::hasText).distinct().toList();
|
||||
for (String transferNo : transferNos) {
|
||||
WarehouseTransferOrder transfer = warehouseTransfers.lockNo(customerId, transferNo);
|
||||
if (transfer != null && "WAIT_OUTBOUND".equals(transfer.getStatus())) {
|
||||
transfer.setStatus("OUTBOUNDED"); transfer.setOutboundAt(now); transfer.setUpdatedAt(now);
|
||||
warehouseTransfers.updateById(transfer);
|
||||
LastlegOrder order = tailOrders.selectById(transfer.getLastlegOrderId());
|
||||
if (order != null && customerId.equals(order.getCustomerId())) {
|
||||
order.setCurrentStatus("IN_TRANSIT");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:OUTBOUNDED"); order.setUpdatedAt(now);
|
||||
tailOrders.updateById(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
|
||||
@@ -724,6 +932,23 @@ public class DeliveryTransferService {
|
||||
if (task != null) { task.setStatus("CANCELLED"); task.setUpdatedAt(now); pdaTasks.updateById(task); }
|
||||
outbound.setStatus("CANCELLED"); outbound.setUpdatedAt(now); outboundOrders.updateById(outbound);
|
||||
for (OutboundPalletItem item : outboundItems) { item.setStatus("CANCELLED"); outboundPallets.updateById(item); }
|
||||
if ("WAREHOUSE_TRANSFER_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
for (String transferNo : outboundItems.stream().map(OutboundPalletItem::getOrderRefs)
|
||||
.filter(StringUtils::hasText).distinct().toList()) {
|
||||
WarehouseTransferOrder transfer = warehouseTransfers.lockNo(customerId, transferNo);
|
||||
if (transfer == null || !"WAIT_OUTBOUND".equals(transfer.getStatus())) continue;
|
||||
String restored = StringUtils.hasText(transfer.getFbaWarehouseCode())
|
||||
? "READY_FOR_DISPATCH" : "WAIT_FBA_DESTINATION";
|
||||
transfer.setStatus(restored); transfer.setUpdatedAt(now); warehouseTransfers.updateById(transfer);
|
||||
LastlegOrder order = tailOrders.selectById(transfer.getLastlegOrderId());
|
||||
if (order != null && customerId.equals(order.getCustomerId())) {
|
||||
order.setCurrentStatus(restored); order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + restored);
|
||||
order.setOutboundOrderNo(null); order.setDeliveryOrderNo(null); order.setUpdatedAt(now);
|
||||
tailOrders.updateById(order);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!existingDeliveries.isEmpty()) {
|
||||
for (LastlegOrder order : tailOrders.selectList(new LambdaQueryWrapper<LastlegOrder>().eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getOutboundOrderNo, outboundNo))) {
|
||||
String from = order.getCurrentStatus(); order.setCurrentStatus("RECEIVED"); order.setSubOrderStatus(null); order.setOutboundOrderNo(null); order.setDeliveryOrderNo(null); order.setUpdatedAt(now); tailOrders.updateById(order);
|
||||
@@ -958,17 +1183,20 @@ public class DeliveryTransferService {
|
||||
candidate.getInboundCode(), candidate.getSkuCode(), candidate.getProductName(),
|
||||
value(candidate.getAvailablePackages()));
|
||||
}
|
||||
private DeliveryResponses.DeliveryRow deliveryRow(DeliveryOrder d) { return new DeliveryResponses.DeliveryRow(d.getId(), d.getCustomerId(), d.getCustomerName(), d.getDeliveryNo(), d.getOutboundNo(), d.getDispatchNo(), d.getOverseasWarehouseId(), d.getOverseasWarehouseName(), d.getStatus(), d.getWaybillNo(), d.getDeliveryMethod(), d.getCarrierName(), d.getPickupMethod(), d.getLoadingType(), d.getVehicleType(), d.getVehicleSize(), d.getVehiclePlate(), d.getDriverName(), d.getDriverPhone(), d.getAppointmentAccount(), d.getSellerName(), value(d.getPalletCount()), value(d.getPackageCount()), d.getTotalWeightKg(), d.getTotalVolumeCbm(), d.getPlannedDeliveryAt(), d.getDispatchedAt(), d.getExpectedArrivalAt(), d.getSignedAt(), d.getRemark(), d.getCreatedAt(), value(d.getVersion()), DeliveryStatusPolicy.allowedActions(d.getStatus()), d.getBolNo(), com.baiyue.lastleg.delivery.model.DeliveryDocumentSnapshot.summary(d.getDocumentsJson()), d.getDestinationWarehouseId(), d.getDestinationWarehouseCode(), d.getDestinationWarehouseName()); }
|
||||
private DeliveryResponses.DeliveryRow deliveryRow(DeliveryOrder d) { return new DeliveryResponses.DeliveryRow(d.getId(), d.getCustomerId(), d.getCustomerName(), d.getDeliveryNo(), d.getOutboundNo(), d.getDispatchNo(), d.getOverseasWarehouseId(), d.getOverseasWarehouseName(), d.getStatus(), d.getWaybillNo(), d.getDeliveryMethod(), d.getCarrierName(), d.getPickupMethod(), d.getLoadingType(), d.getVehicleType(), d.getVehicleSize(), d.getVehiclePlate(), d.getDriverName(), d.getDriverPhone(), d.getAppointmentAccount(), d.getSellerName(), value(d.getPalletCount()), value(d.getPackageCount()), d.getTotalWeightKg(), d.getTotalVolumeCbm(), d.getPlannedDeliveryAt(), d.getDispatchedAt(), d.getExpectedArrivalAt(), d.getSignedAt(), d.getRemark(), d.getCreatedAt(), value(d.getVersion()), DeliveryStatusPolicy.allowedActions(d.getStatus()), d.getBolNo(), com.baiyue.lastleg.delivery.model.DeliveryDocumentSnapshot.summary(d.getDocumentsJson()), d.getDestinationWarehouseId(), d.getDestinationWarehouseCode(), d.getDestinationWarehouseName(), d.getDestinationType(), d.getDeliveryAddress(), d.getContactName(), d.getContactPhone(), d.getPostalCode()); }
|
||||
private DeliveryResponses.DeliverySku sku(LastlegOrderDetail d, LastlegOrder order) { return new DeliveryResponses.DeliverySku(order == null ? null : order.getInboundCode(), order == null ? null : order.getCustomerName(), d.getDestinationWarehouseCode(), null, d.getShipmentNo(), d.getFbaTrackingNo(), d.getSkuCode(), firstText(d.getProductNameZh(), d.getProductNameEn()), value(d.getPackageCount()), d.getGrossWeightKg(), d.getVolumeCbm()); }
|
||||
private DeliveryResponses.DeliveryFeeRow feeRow(OverseasFeeSource f) { return new DeliveryResponses.DeliveryFeeRow(f.getId(), f.getDirection(), f.getFeeItemCode(), f.getOriginalCurrency(), f.getOriginalAmount(), f.getReviewReason()); }
|
||||
private DeliveryResponses.DeliveryOperationRow operationRow(DeliveryOperation o) { return new DeliveryResponses.DeliveryOperationRow(o.getId(), o.getCreatedAt(), o.getOperationNode(), firstText(o.getOperatorName(), o.getOperatorId()), o.getResult()); }
|
||||
private void recordOperation(String customerId, Long deliveryId, String operatorId, String node, String result, LocalDateTime now) { DeliveryOperation operation = new DeliveryOperation(); operation.setCustomerId(customerId); operation.setDeliveryOrderId(deliveryId); operation.setOperationNode(node); operation.setOperatorId(operatorId); operation.setOperatorName(operatorId); operation.setResult(result); operation.setCreatedAt(now); deliveryOperations.insert(operation); }
|
||||
private LastlegOrder firstTailOrder(String customerId, List<DispatchItem> items) { return items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).map(tailOrders::selectById).filter(Objects::nonNull).filter(order -> customerId.equals(order.getCustomerId())).findFirst().orElse(null); }
|
||||
private String deliveryGroupKey(DispatchItem item) {
|
||||
if ("PRIVATE".equals(item.getDestinationType()) && StringUtils.hasText(item.getDeliveryAddress())) {
|
||||
return "PRIVATE:" + item.getDeliveryAddress().trim() + ":" + Objects.toString(item.getPostalCode(), "").trim();
|
||||
}
|
||||
String code = firstText(item.getDestinationWarehouseCode(), item.getFbaWarehouseCode());
|
||||
if (StringUtils.hasText(code)) return "CODE:" + code.toUpperCase(Locale.ROOT);
|
||||
if (StringUtils.hasText(item.getDestinationWarehouseId())) return "ID:" + item.getDestinationWarehouseId().trim();
|
||||
throw conflict("Dispatch pallet is missing its destination FBA warehouse");
|
||||
throw conflict("Dispatch pallet is missing its destination");
|
||||
}
|
||||
private String resolveLxCodes(String customerId, List<DispatchItem> items) {
|
||||
List<String> inboundCodes = items.stream().map(DispatchItem::getInboundCode)
|
||||
@@ -1029,6 +1257,14 @@ public class DeliveryTransferService {
|
||||
return measuredWeight.add(orderWeight);
|
||||
}
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private String hash(Object value) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(objectMapper.writeValueAsString(value).getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (JsonProcessingException | NoSuchAlgorithmException error) {
|
||||
throw new IllegalStateException("无法生成请求摘要", error);
|
||||
}
|
||||
}
|
||||
private static Long parseLong(String value) {
|
||||
try { return StringUtils.hasText(value) ? Long.parseLong(value.trim()) : null; }
|
||||
catch (NumberFormatException ignored) { return null; }
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ public class SmartDispatchController {
|
||||
@RequestParam(required = false) String sourceStage,
|
||||
@RequestParam(required = false) String draftToken) {
|
||||
requirePlatform(user);
|
||||
if (planIds == null || planIds.isEmpty()) {
|
||||
if ("RELABEL".equals(sourceStage) || planIds == null || planIds.isEmpty()) {
|
||||
return ApiResponse.success(service.platformCandidates(orderNo, fbaWarehouseCode, sourceStage, draftToken));
|
||||
}
|
||||
return ApiResponse.success(service.candidates(service.resolveCustomerScopeForPlans(planIds), planIds, orderNo,
|
||||
|
||||
@@ -30,6 +30,11 @@ public class DispatchItem {
|
||||
private String destinationWarehouseCode;
|
||||
private String destinationWarehouseName;
|
||||
private String fbaWarehouseCode;
|
||||
private String destinationType;
|
||||
private String deliveryAddress;
|
||||
private String postalCode;
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private Integer packageCount;
|
||||
private BigDecimal volumeCbm;
|
||||
/** PLAN for planned pallets; RECEIVED for warehouse/entity pallets. */
|
||||
|
||||
+16
-6
@@ -64,16 +64,20 @@ public interface DispatchCandidateMapper {
|
||||
"JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id " +
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks," +
|
||||
"MIN(l.destination_type) AS destinationType,MIN(l.delivery_address) AS deliveryAddress," +
|
||||
"MIN(l.postal_code) AS postalCode,MIN(l.contact_name) AS contactName,MIN(l.contact_phone) AS contactPhone " +
|
||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
||||
"WHERE 1=1 " +
|
||||
"WHERE ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(l.destination_type='PRIVATE' AND NULLIF(TRIM(l.delivery_address),'') IS NOT NULL AND NULLIF(TRIM(l.postal_code),'') IS NOT NULL " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)) " +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND ip.customer_id=#{customerId}</if> " +
|
||||
"<if test='planIds != null and planIds.size() > 0'> AND p.plan_id IN " +
|
||||
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></if> " +
|
||||
"<if test='orderNo != null and orderNo != \"\"'> AND o.order_no LIKE CONCAT('%',#{orderNo},'%')</if> " +
|
||||
"<if test='orderNo != null and orderNo != \"\"'> AND (o.order_no LIKE CONCAT('%',#{orderNo},'%') OR l.inbound_code LIKE CONCAT('%',#{orderNo},'%'))</if> " +
|
||||
"<if test='fbaWarehouseCode != null and fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{fbaWarehouseCode}</if> " +
|
||||
"<if test='sourceStage != null and sourceStage != \"\"'> AND " +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END=#{sourceStage}</if> " +
|
||||
@@ -146,13 +150,18 @@ public interface DispatchCandidateMapper {
|
||||
"JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id " +
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks," +
|
||||
"MIN(l.destination_type) AS destinationType,MIN(l.delivery_address) AS deliveryAddress," +
|
||||
"MIN(l.postal_code) AS postalCode,MIN(l.contact_name) AS contactName,MIN(l.contact_phone) AS contactPhone " +
|
||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
||||
"WHERE ip.customer_id=#{customerId} AND p.id IN " +
|
||||
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(l.destination_type='PRIVATE' AND NULLIF(TRIM(l.delivery_address),'') IS NOT NULL AND NULLIF(TRIM(l.postal_code),'') IS NOT NULL " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation ha " +
|
||||
"JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " +
|
||||
"JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " +
|
||||
@@ -184,7 +193,8 @@ public interface DispatchCandidateMapper {
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS fbaWarehouseCode," +
|
||||
"SUM(ri.notified_package_count) AS packageCount,MAX(wtp.volume_cbm) AS volumeCbm,'RELABEL' AS sourceStage," +
|
||||
"NULL AS cartonMarks,GROUP_CONCAT(DISTINCT COALESCE(NULLIF(ri.fba_carton_mark,''),ri.carton_mark) " +
|
||||
"ORDER BY COALESCE(NULLIF(ri.fba_carton_mark,''),ri.carton_mark) SEPARATOR ',') AS relabelCartonMarks " +
|
||||
"ORDER BY COALESCE(NULLIF(ri.fba_carton_mark,''),ri.carton_mark) SEPARATOR ',') AS relabelCartonMarks," +
|
||||
"'FBA' AS destinationType,NULL AS deliveryAddress,NULL AS postalCode,NULL AS contactName,NULL AS contactPhone " +
|
||||
"FROM ll_relabel_application a JOIN ll_relabel_item ri ON ri.application_id=a.id AND ri.customer_id=a.customer_id " +
|
||||
"JOIN ll_inventory_position pos ON pos.id=ri.source_inventory_position_id AND pos.customer_id=a.customer_id " +
|
||||
"LEFT JOIN ll_warehouse_transfer_order wto ON wto.relabel_no=a.relabel_no AND wto.customer_id=a.customer_id " +
|
||||
@@ -193,7 +203,7 @@ public interface DispatchCandidateMapper {
|
||||
"WHERE a.delivery_method='TRUCK' AND a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND a.customer_id=#{customerId} </if>" +
|
||||
"<if test='relabelNo != null and relabelNo != \"\"'>AND a.relabel_no=#{relabelNo} </if>" +
|
||||
"<if test='orderNo != null and orderNo != \"\"'>AND a.relabel_no LIKE CONCAT('%',#{orderNo},'%') </if>" +
|
||||
"<if test='orderNo != null and orderNo != \"\"'>AND (a.relabel_no LIKE CONCAT('%',#{orderNo},'%') OR ri.source_inbound_code LIKE CONCAT('%',#{orderNo},'%') OR ri.source_inbound_order_no LIKE CONCAT('%',#{orderNo},'%')) </if>" +
|
||||
"AND pos.hold_packages > 0 " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di JOIN ll_dispatch_order existing_dispatch " +
|
||||
"ON existing_dispatch.id=di.dispatch_order_id AND existing_dispatch.customer_id=di.customer_id " +
|
||||
|
||||
+8
-3
@@ -30,7 +30,12 @@ public record DispatchPalletCandidate(
|
||||
BigDecimal volumeCbm,
|
||||
String sourceStage,
|
||||
String cartonMarks,
|
||||
String relabelCartonMarks
|
||||
String relabelCartonMarks,
|
||||
String destinationType,
|
||||
String deliveryAddress,
|
||||
String postalCode,
|
||||
String contactName,
|
||||
String contactPhone
|
||||
) {
|
||||
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId,
|
||||
String overseasWarehouseId, String overseasWarehouseName, String orderNo,
|
||||
@@ -41,7 +46,7 @@ public record DispatchPalletCandidate(
|
||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, overseasWarehouseId,
|
||||
overseasWarehouseName, orderNo, orderRefs, inboundCode, palletNo, destinationWarehouseId,
|
||||
destinationWarehouseCode, destinationWarehouseName, fbaWarehouseCode, packageCount, volumeCbm,
|
||||
sourceStage, null, null);
|
||||
sourceStage, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId,
|
||||
@@ -51,7 +56,7 @@ public record DispatchPalletCandidate(
|
||||
BigDecimal volumeCbm, String sourceStage) {
|
||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, null, null, orderNo, orderRefs, inboundCode,
|
||||
palletNo, destinationWarehouseId, destinationWarehouseCode, destinationWarehouseName,
|
||||
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null, null);
|
||||
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null, null, null, null, null, null, null);
|
||||
}
|
||||
public String sourcePalletKey() {
|
||||
// Once received, the physical pallet is the shared resource even when it contains
|
||||
|
||||
+32
-5
@@ -66,6 +66,7 @@ public class DispatchService {
|
||||
@Resource private LastlegOrderMapper tailOrders;
|
||||
@Resource private OutboundOrderMapper outboundOrders;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard relabelBindingGuard;
|
||||
|
||||
public String resolveCustomerScopeForPlans(List<Long> planIds) {
|
||||
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
|
||||
@@ -107,12 +108,14 @@ public class DispatchService {
|
||||
public List<DispatchPalletCandidate> platformCandidates(String orderNo, String fbaWarehouseCode,
|
||||
String sourceStage, String draftToken) {
|
||||
validateSourceStage(sourceStage);
|
||||
List<DispatchPalletCandidate> result = candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
|
||||
List<DispatchPalletCandidate> result = "RELABEL".equals(sourceStage) ? List.of() : candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
|
||||
trimToNull(sourceStage), trimToNull(draftToken));
|
||||
if (!StringUtils.hasText(sourceStage) || "RECEIVED".equals(sourceStage)) {
|
||||
if (!StringUtils.hasText(sourceStage) || Set.of("RECEIVED", "RELABEL").contains(sourceStage)) {
|
||||
List<DispatchPalletCandidate> relabel = candidates.relabelAvailable(null, trimToNull(orderNo), null);
|
||||
if (relabel != null && !relabel.isEmpty()) {
|
||||
result = java.util.stream.Stream.concat(result.stream(), relabel.stream()).toList();
|
||||
result = java.util.stream.Stream.concat(result.stream(), relabel.stream().filter(item ->
|
||||
!StringUtils.hasText(fbaWarehouseCode) || Objects.toString(item.fbaWarehouseCode(), "")
|
||||
.toUpperCase(java.util.Locale.ROOT).contains(fbaWarehouseCode.trim().toUpperCase(java.util.Locale.ROOT)))).toList();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -270,6 +273,11 @@ public class DispatchService {
|
||||
if (selected.size() != palletIds.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more selected pallets are not in this tenant");
|
||||
}
|
||||
if (selected.stream().anyMatch(candidate -> !hasValidDestination(candidate))) {
|
||||
boolean legacyMissingFba = selected.stream().allMatch(candidate -> !"PRIVATE".equals(candidate.destinationType()));
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, legacyMissingFba
|
||||
? "所选托盘尚未设置 FBA 仓,不能智能排车" : "所选托盘尚未设置有效目的地,不能智能排车");
|
||||
}
|
||||
assertNoDetainedOrders(customerId, selected);
|
||||
boolean missingWarehouse = selected.stream().map(DispatchPalletCandidate::overseasWarehouseId)
|
||||
.anyMatch(value -> value == null || value.isBlank());
|
||||
@@ -396,6 +404,11 @@ public class DispatchService {
|
||||
item.setDestinationWarehouseCode(candidate.destinationWarehouseCode());
|
||||
item.setDestinationWarehouseName(candidate.destinationWarehouseName());
|
||||
item.setFbaWarehouseCode(candidate.fbaWarehouseCode());
|
||||
item.setDestinationType(candidate.destinationType());
|
||||
item.setDeliveryAddress(candidate.deliveryAddress());
|
||||
item.setPostalCode(candidate.postalCode());
|
||||
item.setContactName(candidate.contactName());
|
||||
item.setContactPhone(candidate.contactPhone());
|
||||
item.setPackageCount(Objects.requireNonNullElse(candidate.packageCount(), 0));
|
||||
item.setVolumeCbm(Objects.requireNonNullElse(candidate.volumeCbm(), BigDecimal.ZERO));
|
||||
item.setSourceStage(candidate.sourceStage());
|
||||
@@ -433,6 +446,7 @@ public class DispatchService {
|
||||
if (application == null || !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标尚未完成或客户尚未确认,无法进行排车");
|
||||
}
|
||||
relabelBindingGuard.requireBound(application);
|
||||
List<DispatchPalletCandidate> selected = candidates.relabelAvailable(customerId, null, request.relabelNo());
|
||||
Set<Long> requested = request.planPalletIds().stream().map(Long::valueOf).collect(Collectors.toSet());
|
||||
selected = selected.stream().filter(c -> requested.contains(c.warehousePalletId())).toList();
|
||||
@@ -653,10 +667,23 @@ public class DispatchService {
|
||||
}
|
||||
|
||||
private static String candidateFbaCode(DispatchPalletCandidate candidate) {
|
||||
if ("PRIVATE".equals(candidate.destinationType())) {
|
||||
return java.util.stream.Stream.of(candidate.deliveryAddress(), candidate.postalCode())
|
||||
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.joining(" "));
|
||||
}
|
||||
return StringUtils.hasText(candidate.fbaWarehouseCode())
|
||||
? candidate.fbaWarehouseCode().trim() : trimToNull(candidate.destinationWarehouseCode());
|
||||
}
|
||||
|
||||
private static boolean hasValidDestination(DispatchPalletCandidate candidate) {
|
||||
if ("PRIVATE".equals(candidate.destinationType())) {
|
||||
return StringUtils.hasText(candidate.deliveryAddress()) && StringUtils.hasText(candidate.postalCode())
|
||||
&& StringUtils.hasText(candidate.contactName()) && StringUtils.hasText(candidate.contactPhone());
|
||||
}
|
||||
return StringUtils.hasText(candidate.fbaWarehouseCode())
|
||||
|| StringUtils.hasText(candidate.destinationWarehouseCode());
|
||||
}
|
||||
|
||||
private static String itemFbaCode(DispatchItem item) {
|
||||
return StringUtils.hasText(item.getFbaWarehouseCode())
|
||||
? item.getFbaWarehouseCode().trim() : trimToNull(item.getDestinationWarehouseCode());
|
||||
@@ -716,8 +743,8 @@ public class DispatchService {
|
||||
}
|
||||
|
||||
private static void validateSourceStage(String sourceStage) {
|
||||
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
|
||||
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED", "RELABEL").contains(sourceStage)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN, RECEIVED or RELABEL");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,12 @@ public record LastlegOrderSummary(
|
||||
String nodeSource,
|
||||
int version,
|
||||
Integer palletCount,
|
||||
@Schema(description = "客户预报箱数")
|
||||
Integer packageCount,
|
||||
@Schema(description = "PDA 实收箱数;尚未完成收货时为空")
|
||||
Integer receivedPackageCount,
|
||||
@Schema(description = "实收减预报箱数;尚未完成收货时为空")
|
||||
Integer packageDifference,
|
||||
BigDecimal volumeCbm,
|
||||
Integer productQuantity,
|
||||
BigDecimal volumeWeightKg,
|
||||
|
||||
@@ -44,6 +44,7 @@ public class LastlegOrder {
|
||||
private Integer version;
|
||||
private Integer palletCount;
|
||||
private Integer packageCount;
|
||||
private Integer receivedPackageCount;
|
||||
private Integer pickedPackageCount;
|
||||
private Integer shippedPackageCount;
|
||||
private BigDecimal volumeCbm;
|
||||
|
||||
+4
@@ -21,6 +21,10 @@ public class InboundPlanLine {
|
||||
private String destinationWarehouseId;
|
||||
private String destinationWarehouseCode;
|
||||
private String destinationWarehouseName;
|
||||
private String deliveryAddress;
|
||||
private String postalCode;
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private BigDecimal volumeCbm;
|
||||
private Integer requiredPackageCount;
|
||||
private Integer plannedPalletCount;
|
||||
|
||||
+5
@@ -22,6 +22,11 @@ public class InboundPlanPallet {
|
||||
private String destinationWarehouseId;
|
||||
private String destinationWarehouseCode;
|
||||
private String destinationWarehouseName;
|
||||
private String destinationType;
|
||||
private String deliveryAddress;
|
||||
private String postalCode;
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private BigDecimal capacityCbm;
|
||||
private BigDecimal allocatedVolumeCbm;
|
||||
private Integer sequenceNo;
|
||||
|
||||
+23
@@ -6,6 +6,8 @@ import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
@@ -293,6 +295,27 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
return palletBatch("/internal/v1/warehouse-pallet-batches/compensate", request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintableFile printPalletLabel(long warehouseId, long palletId, String transferNo, String requestId) {
|
||||
ResponseEntity<byte[]> response = client.get()
|
||||
.uri(builder -> builder.path("/internal/v1/warehouse-pallets/{warehouseId}/{palletId}/label")
|
||||
.queryParam("transferNo", transferNo).build(warehouseId, palletId))
|
||||
.header("X-Internal-Service-Token", internalToken)
|
||||
.header("X-Request-Id", requestId == null ? "" : requestId)
|
||||
.retrieve().onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw new ResponseStatusException(result.getStatusCode().is4xxClientError()
|
||||
? HttpStatus.NOT_FOUND : HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"Warehouse pallet label printing failed");
|
||||
}).toEntity(byte[].class);
|
||||
byte[] content = response.getBody() == null ? new byte[0] : response.getBody();
|
||||
if (content.length == 0) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"Warehouse pallet label printing returned an empty PDF");
|
||||
String fileName = response.getHeaders().getContentDisposition().getFilename();
|
||||
String contentType = response.getHeaders().getContentType() == null
|
||||
? MediaType.APPLICATION_PDF_VALUE : response.getHeaders().getContentType().toString();
|
||||
return new PrintableFile(content, fileName == null ? "pallet-label.pdf" : fileName, contentType);
|
||||
}
|
||||
|
||||
private PalletBatchResponse palletBatch(String path, Object body) {
|
||||
PalletBatchEnvelope response = client.post().uri(path)
|
||||
.header("X-Internal-Service-Token", internalToken).body(body).retrieve()
|
||||
|
||||
+4
@@ -39,6 +39,9 @@ public interface WarehouseResourceGateway {
|
||||
default PalletBatchResponse compensatePalletBatch(PalletBatchCompensation request) {
|
||||
throw new UnsupportedOperationException("Warehouse pallet batch compensation is not configured");
|
||||
}
|
||||
default PrintableFile printPalletLabel(long warehouseId, long palletId, String transferNo, String requestId) {
|
||||
throw new UnsupportedOperationException("Warehouse pallet label printing is not configured");
|
||||
}
|
||||
|
||||
record WarehousePalletReference(long palletId, String palletNo, long warehouseId, long locationId,
|
||||
String status, String fbaWarehouseCode, java.math.BigDecimal unitPrice, int version) {
|
||||
@@ -67,4 +70,5 @@ public interface WarehouseResourceGateway {
|
||||
record PalletBatchItem(long customerId, String businessKey, String inboundPlanNo, int sequenceNo,
|
||||
long palletId, String palletCode, long locationId, long resourceLockId,
|
||||
String status) { }
|
||||
record PrintableFile(byte[] content, String fileName, String contentType) { }
|
||||
}
|
||||
|
||||
-3
@@ -511,9 +511,6 @@ public class InventoryCommandService {
|
||||
String skuCode = sku(requestedSkuCode);
|
||||
String destinationId = trim(destinationWarehouseId);
|
||||
String destinationCode = trim(destinationWarehouseCode);
|
||||
if (destinationId == null && destinationCode == null) {
|
||||
throw badRequest("Destination warehouse ID or code is required");
|
||||
}
|
||||
if (destinationCode == null) destinationCode = "";
|
||||
InventoryBalance balance = balances.lockDimension(customerId, frozenWarehouseId,
|
||||
destinationCode, inboundOrderId, inboundCode);
|
||||
|
||||
@@ -2,9 +2,22 @@ package com.baiyue.lastleg.model;
|
||||
|
||||
/** Standard main-process statuses for a last-leg order. */
|
||||
public enum LastlegOrderStatus {
|
||||
PENDING_REVIEW("待审核"),
|
||||
INBOUND_MEASURING("入库测量中"),
|
||||
PENDING_ASSIGNMENT("待分配"),
|
||||
PENDING_INBOUND("待入库"),
|
||||
INBOUND_COMPLETED("入库完成"),
|
||||
WAIT_RELABEL("待换标"),
|
||||
WAIT_FBA_DESTINATION("待确认目的仓"),
|
||||
READY_FOR_DISPATCH("待创建出库单"),
|
||||
WAIT_OUTBOUND("待出库"),
|
||||
ALLOCATED("已分配库存"),
|
||||
OUTBOUNDED("已出库"),
|
||||
PARTIALLY_RECEIVED("部分收货"),
|
||||
RECEIVED("已收货"),
|
||||
IN_TRANSIT("运输中"),
|
||||
COMPLETED("已完成"),
|
||||
CANCELLED("已取消"),
|
||||
PENDING_DELIVERY("待派送"),
|
||||
DELIVERING("派送中"),
|
||||
DELIVERED("派送完成");
|
||||
|
||||
+7
@@ -28,6 +28,13 @@ public class PdaPalletOperationController {
|
||||
return ApiResponse.success(service.resolve(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/relabel-sources")
|
||||
public ApiResponse<java.util.List<PalletOperationResponses.RelabelSource>> relabelSources(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.RelabelSources request) {
|
||||
return ApiResponse.success(service.relabelSources(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/relabel-pallet-label")
|
||||
public ApiResponse<PalletOperationResponses.PrintablePallet> createRelabelPallet(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+9
-2
@@ -13,6 +13,7 @@ public final class PalletOperationRequests {
|
||||
private PalletOperationRequests() { }
|
||||
|
||||
public record Scan(@NotBlank @Size(max = 128) String code) { }
|
||||
public record RelabelSources(@NotBlank @Size(max = 64) String relabelNo) { }
|
||||
public record NewRelabelPallet(@NotBlank @Size(max = 64) String relabelNo,
|
||||
@NotBlank @Size(max = 128) String idempotencyKey) { }
|
||||
public record PalletExpectation(@NotBlank @Size(max = 128) String palletCode,
|
||||
@@ -33,13 +34,19 @@ public final class PalletOperationRequests {
|
||||
@NotEmpty @Size(max = 20) List<@Valid TargetMove> targets,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
/** 换标场景按箱唛拆托并绑定到一个目标托盘。箱唛位置由服务端从换标单快照解析。 */
|
||||
/** actualSource为现场确认的原入仓库存行;cartonMarks确认本批目标绑定,不推断历史箱唛位置。 */
|
||||
public record RelabelDisassemble(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@NotBlank @Size(max = 64) String relabelNo,
|
||||
@Valid PalletExpectation source,
|
||||
@Valid PalletExpectation target,
|
||||
@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> cartonMarks,
|
||||
@Size(max = 500) String reason) { }
|
||||
@Size(max = 500) String reason,
|
||||
@Valid MoveLine actualSource) {
|
||||
public RelabelDisassemble(String idempotencyKey, String relabelNo, PalletExpectation source,
|
||||
PalletExpectation target, List<String> cartonMarks, String reason) {
|
||||
this(idempotencyKey, relabelNo, source, target, cartonMarks, reason, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record Swap(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@Valid PalletExpectation source,
|
||||
|
||||
+3
@@ -5,6 +5,9 @@ import java.util.List;
|
||||
|
||||
public final class PalletOperationResponses {
|
||||
private PalletOperationResponses() { }
|
||||
public record RelabelSource(long positionId, long positionVersion, String palletCode,
|
||||
String locationCode, String inboundCode, long stockPackages,
|
||||
long movablePackages, List<String> cartonMarks) { }
|
||||
|
||||
public record InventoryLine(String customerId, String customerName, long balanceId, long positionId,
|
||||
String inboundCode, String skuCode, String productName,
|
||||
|
||||
+6
@@ -23,6 +23,12 @@ public interface PdaPalletOperationMapper extends BaseMapper<PdaPalletOperation>
|
||||
PdaPalletOperation findOperation(@Param("operatorKey") String operatorKey,
|
||||
@Param("idempotencyKey") String idempotencyKey);
|
||||
|
||||
@Select(CONTENT_SELECT + "WHERE b.customer_id=#{customerId} AND b.overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND b.id=#{balanceId} AND p.pallet_id>0 " +
|
||||
"AND (p.available_packages+p.hold_packages)>0 ORDER BY p.pallet_no,p.id")
|
||||
List<PalletInventoryRow> relabelSources(@Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId, @Param("balanceId") long balanceId);
|
||||
|
||||
@Select(CONTENT_SELECT + "WHERE b.overseas_warehouse_id=#{warehouseId} AND p.pallet_id=#{palletId} " +
|
||||
"AND (p.available_packages+p.hold_packages+p.allocated_packages+p.picked_packages+p.receiving_packages)>0 " +
|
||||
"ORDER BY b.customer_id,b.inbound_code,p.id")
|
||||
|
||||
+211
-15
@@ -78,13 +78,42 @@ public class PdaPalletOperationService {
|
||||
return content(identity.overseasWarehouseId(), pallet);
|
||||
}
|
||||
|
||||
public List<PalletOperationResponses.RelabelSource> relabelSources(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.RelabelSources request) {
|
||||
RelabelApplication app = relabelApplications.findAnyByRelabelNo(request.relabelNo());
|
||||
requireRelabelWork(identity, app);
|
||||
Set<String> boundMarks = boundRelabelMarks(app);
|
||||
List<RelabelItem> items = relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
|
||||
.eq("application_id", app.getId()).eq("customer_id", app.getCustomerId()));
|
||||
Map<Long, List<RelabelItem>> byBalance = new LinkedHashMap<>();
|
||||
for (RelabelItem item : items) {
|
||||
InventoryPosition anchor = positions.selectById(item.getSourceInventoryPositionId());
|
||||
if (anchor == null || !Objects.equals(anchor.getCustomerId(), app.getCustomerId())
|
||||
|| !Objects.equals(anchor.getOverseasWarehouseId(), app.getOverseasWarehouseId()))
|
||||
throw conflict("换标单库存来源缺失,请核对原入仓单");
|
||||
byBalance.computeIfAbsent(anchor.getBalanceId(), ignored -> new ArrayList<>()).add(item);
|
||||
}
|
||||
List<PalletOperationResponses.RelabelSource> result = new ArrayList<>();
|
||||
byBalance.forEach((balanceId, group) -> {
|
||||
for (PalletInventoryRow row : mapper.relabelSources(app.getCustomerId(), app.getOverseasWarehouseId(), balanceId)) {
|
||||
long ownHold = group.stream().filter(item -> Objects.equals(item.getSourceInventoryPositionId(), row.getPositionId()))
|
||||
.mapToLong(item -> value(item.getNotifiedPackageCount())).sum();
|
||||
result.add(new PalletOperationResponses.RelabelSource(row.getPositionId(), value(row.getVersion()),
|
||||
row.getPalletNo(), row.getWarehouseLocationCode(), row.getInboundCode(),
|
||||
value(row.getAvailablePackages()) + value(row.getHoldPackages()) + value(row.getAllocatedPackages())
|
||||
+ value(row.getPickedPackages()),
|
||||
value(row.getAvailablePackages()) + Math.min(ownHold, value(row.getHoldPackages())),
|
||||
group.stream().map(RelabelItem::getCartonMark).filter(mark -> !boundMarks.contains(mark)).toList()));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public PalletOperationResponses.PrintablePallet createRelabelPallet(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.NewRelabelPallet request) {
|
||||
String relabelNo = required(request.relabelNo(), "换标单号不能为空");
|
||||
RelabelApplication application = relabelApplications.findAnyByRelabelNo(relabelNo);
|
||||
if (application == null || !"READY_FOR_OUTBOUND".equals(application.getStatus())) {
|
||||
throw conflict("换标单尚未完成客户确认,不能生成新托盘");
|
||||
}
|
||||
requireRelabelWork(identity, application);
|
||||
if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
|
||||
throw conflict("换标单不属于 Token 冻结的海外仓");
|
||||
}
|
||||
@@ -153,9 +182,7 @@ public class PdaPalletOperationService {
|
||||
if (replay != null) return replay;
|
||||
String relabelNo = required(request.relabelNo(), "换标单号不能为空");
|
||||
RelabelApplication application = relabelApplications.findAnyByRelabelNo(relabelNo);
|
||||
if (application == null || !"READY_FOR_OUTBOUND".equals(application.getStatus())) {
|
||||
throw conflict("换标单尚未完成客户确认,不能执行拆托");
|
||||
}
|
||||
requireRelabelWork(identity, application);
|
||||
if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
|
||||
throw conflict("换标单不属于 Token 冻结的海外仓");
|
||||
}
|
||||
@@ -177,29 +204,61 @@ public class PdaPalletOperationService {
|
||||
}
|
||||
Map<Long, Long> packagesByPosition = new LinkedHashMap<>();
|
||||
Map<Long, List<RelabelItem>> itemsByPosition = new LinkedHashMap<>();
|
||||
Map<Long, InventoryPosition> anchors = new LinkedHashMap<>();
|
||||
InventoryPosition actual = request.actualSource() == null ? null : positions.selectById(request.actualSource().positionId());
|
||||
if (request.actualSource() != null && (actual == null
|
||||
|| !Objects.equals(actual.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(actual.getPalletId(), source.palletId())
|
||||
|| !Objects.equals(actual.getOverseasWarehouseId(), application.getOverseasWarehouseId())
|
||||
|| value(actual.getVersion()) != request.actualSource().expectedPositionVersion()))
|
||||
throw conflict("实际来源托盘库存已变化,请刷新后确认");
|
||||
if (actual != null && application.getOutboundOrderNo() != null && !application.getOutboundOrderNo().isBlank())
|
||||
throw conflict("已生成出库单,请先处理出库单,不能重新分配冻结来源");
|
||||
if (actual != null && marks.stream().anyMatch(boundRelabelMarks(application)::contains))
|
||||
throw conflict("本批包含本轮已组托的换标箱,请刷新后选择未组托的箱");
|
||||
if ("PROCESSING".equals(application.getStatus())) {
|
||||
var workTask = relabelWorkTasks.find(application.getCustomerId(), application.getId());
|
||||
for (RelabelItem item : selected) {
|
||||
Long completed = relabelWorkFacts.selectCount(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact>()
|
||||
.eq("task_id", workTask.getId()).eq("customer_id", application.getCustomerId())
|
||||
.eq("item_id", item.getId()).eq("result", "RELABELLED"));
|
||||
if (completed == null || completed < value(item.getNotifiedPackageCount()))
|
||||
throw conflict("请先完成本箱全部换标作业,再进行组托");
|
||||
}
|
||||
}
|
||||
for (RelabelItem item : selected) {
|
||||
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null
|
||||
|| item.getNotifiedPackageCount() <= 0) throw conflict("换标箱唛缺少库存位置或箱数快照");
|
||||
InventoryPosition position = positions.selectById(item.getSourceInventoryPositionId());
|
||||
if (position == null || !Objects.equals(position.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(position.getPalletId(), source.palletId())
|
||||
|| (actual == null && !Objects.equals(position.getPalletId(), source.palletId()))
|
||||
|| (actual != null && (!Objects.equals(position.getBalanceId(), actual.getBalanceId())
|
||||
|| !Objects.equals(position.getBatchNo(), actual.getBatchNo())))
|
||||
|| !Objects.equals(position.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
|
||||
throw conflict("扫描的原托盘与换标箱唛实际库存位置不一致");
|
||||
throw conflict("来源托盘必须属于本次拆托的原入仓单和库存批次");
|
||||
}
|
||||
packagesByPosition.merge(position.getId(), item.getNotifiedPackageCount().longValue(), Long::sum);
|
||||
itemsByPosition.computeIfAbsent(position.getId(), ignored -> new ArrayList<>()).add(item);
|
||||
anchors.put(position.getId(), position);
|
||||
Long sourcePosition = actual == null ? position.getId() : actual.getId();
|
||||
packagesByPosition.merge(sourcePosition, item.getNotifiedPackageCount().longValue(), Long::sum);
|
||||
itemsByPosition.computeIfAbsent(sourcePosition, ignored -> new ArrayList<>()).add(item);
|
||||
}
|
||||
if (actual != null && request.actualSource().packages() != packagesByPosition.get(actual.getId()))
|
||||
throw conflict("拆出箱数必须等于本次确认绑定的换标箱数");
|
||||
List<PlannedMove> moves = packagesByPosition.entrySet().stream().map(entry -> {
|
||||
InventoryPosition position = positions.selectById(entry.getKey());
|
||||
if (value(position.getHoldPackages()) < entry.getValue()) throw conflict("换标冻结库存箱数不足");
|
||||
long own = selected.stream().filter(item -> Objects.equals(item.getSourceInventoryPositionId(), position.getId()))
|
||||
.mapToLong(item -> value(item.getNotifiedPackageCount())).sum();
|
||||
if (actual == null ? value(position.getHoldPackages()) < entry.getValue()
|
||||
: value(position.getHoldPackages()) < own || value(position.getAvailablePackages()) < entry.getValue() - own)
|
||||
throw conflict("来源托盘可拆箱数不足,不能占用其他单据冻结或已分配库存");
|
||||
return new PlannedMove(entry.getKey(), entry.getValue(), value(position.getVersion()), source.palletId(),
|
||||
source.palletCode(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), target.locationId()), InventoryBucket.HOLD);
|
||||
}).toList();
|
||||
requireCommonDestination(identity.overseasWarehouseId(), moves, List.of(target));
|
||||
requireRelabelDestination(identity.overseasWarehouseId(), selected, target);
|
||||
Plan base = plan(identity.overseasWarehouseId(), pallets, moves, Set.of(target.palletId()), false);
|
||||
Plan relabelPlan = new Plan(base.pallets(), base.moves(), base.changes(),
|
||||
new RelabelMutation(application, itemsByPosition));
|
||||
new RelabelMutation(application, itemsByPosition, actual == null ? Map.of() : anchors));
|
||||
String reason = "换标拆托" + (request.reason() == null || request.reason().isBlank()
|
||||
? "" : ":" + request.reason().trim());
|
||||
return execute(identity, "RELABEL_PALLET_DISASSEMBLE", request.idempotencyKey(), request,
|
||||
@@ -288,6 +347,10 @@ public class PdaPalletOperationService {
|
||||
throw conflict("库存所在托盘已变化,请重新扫码");
|
||||
}
|
||||
if (value(locked.getVersion()) != move.expectedVersion()) throw conflict("库存版本已变化,请重新扫码");
|
||||
if (plan.relabelMutation() != null && !plan.relabelMutation().anchors().isEmpty()) {
|
||||
reconcileRelabelHold(identity, operation, plan.relabelMutation(), move, key);
|
||||
locked = mapper.lockPosition(String.valueOf(identity.overseasWarehouseId()), move.positionId());
|
||||
}
|
||||
if (move.bucket() == InventoryBucket.HOLD) requireHeld(locked, move.packages());
|
||||
else requireMovable(locked, move.packages());
|
||||
customers.add(locked.getCustomerId());
|
||||
@@ -296,6 +359,8 @@ public class PdaPalletOperationService {
|
||||
}
|
||||
if (plan.relabelMutation() != null) relabelPalletMutations.moveSelectedItems(
|
||||
plan.relabelMutation().application(), plan.relabelMutation().itemsByPosition(), relabelTargets);
|
||||
if (plan.relabelMutation() != null)
|
||||
recordRelabelBinding(identity, plan.relabelMutation(), relabelTargets);
|
||||
for (PalletChange change : plan.changes()) {
|
||||
long actual = mapper.palletPackages(String.valueOf(identity.overseasWarehouseId()), change.pallet().palletId());
|
||||
boolean shouldBeEmpty = "IDLE".equals(change.targetStatus());
|
||||
@@ -320,7 +385,8 @@ public class PdaPalletOperationService {
|
||||
|
||||
private void lockRelabelMutation(RelabelMutation mutation, long warehouseId) {
|
||||
RelabelApplication locked = relabelApplications.lockAnyByRelabelNo(mutation.application().getRelabelNo());
|
||||
if (locked == null || !"READY_FOR_OUTBOUND".equals(locked.getStatus())
|
||||
if (locked != null) requireUnallocatedRelabel(locked);
|
||||
if (locked == null || !Objects.equals(locked.getStatus(), mutation.application().getStatus())
|
||||
|| !Objects.equals(locked.getCustomerId(), mutation.application().getCustomerId())
|
||||
|| !Objects.equals(locked.getOverseasWarehouseId(), String.valueOf(warehouseId))
|
||||
|| !Objects.equals(locked.getOutboundOrderNo(), mutation.application().getOutboundOrderNo())) {
|
||||
@@ -328,6 +394,52 @@ public class PdaPalletOperationService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Redistributes only this application's hold within the same inbound balance; physical totals stay unchanged. */
|
||||
private void reconcileRelabelHold(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
RelabelMutation mutation, PlannedMove move, String key) {
|
||||
requireRelabelWork(identity, mutation.application());
|
||||
List<RelabelItem> selected = mutation.itemsByPosition().get(move.positionId());
|
||||
Set<String> alreadyBound = boundRelabelMarks(mutation.application());
|
||||
if (selected.stream().anyMatch(item -> alreadyBound.contains(item.getCartonMark())))
|
||||
throw conflict("换标箱已在本轮组托,请刷新后重试");
|
||||
requireRelabelDestination(identity.overseasWarehouseId(), selected, physical.resolve(move.targetPalletCode()));
|
||||
Map<Long, Long> release = new LinkedHashMap<>();
|
||||
for (RelabelItem item : selected) {
|
||||
RelabelItem current = relabelItems.selectById(item.getId());
|
||||
if (current == null || !Objects.equals(current.getSourceInventoryPositionId(), item.getSourceInventoryPositionId())
|
||||
|| !Objects.equals(current.getNotifiedPackageCount(), item.getNotifiedPackageCount()))
|
||||
throw conflict("换标箱的绑定已变化,请刷新后重试");
|
||||
if (!Objects.equals(item.getSourceInventoryPositionId(), move.positionId()))
|
||||
release.merge(item.getSourceInventoryPositionId(), (long) value(item.getNotifiedPackageCount()), Long::sum);
|
||||
}
|
||||
InventoryPosition actual = positions.selectById(move.positionId());
|
||||
long required = release.values().stream().mapToLong(Long::longValue).sum();
|
||||
if (value(actual.getAvailablePackages()) < required) throw conflict("实际来源托盘可用箱数不足");
|
||||
InventoryBalance balance = balances.selectById(actual.getBalanceId());
|
||||
for (var entry : release.entrySet()) {
|
||||
PalletInventoryRow locked = mapper.lockPosition(mutation.application().getOverseasWarehouseId(), entry.getKey());
|
||||
InventoryPosition snapshot = mutation.anchors().get(entry.getKey());
|
||||
if (locked == null || value(locked.getVersion()) != value(snapshot.getVersion())
|
||||
|| !Objects.equals(locked.getBalanceId(), actual.getBalanceId())
|
||||
|| value(locked.getHoldPackages()) < entry.getValue())
|
||||
throw conflict("原冻结库存已变化,请刷新后重试");
|
||||
InventoryPosition anchor = positions.selectById(entry.getKey());
|
||||
adjustRelabelHold(identity, operation, balance, anchor, -entry.getValue(), key + ":RELEASE:" + anchor.getId());
|
||||
}
|
||||
if (required > 0) adjustRelabelHold(identity, operation, balance, actual, required, key + ":FREEZE:" + actual.getId());
|
||||
}
|
||||
|
||||
private void adjustRelabelHold(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
InventoryBalance balance, InventoryPosition position, long delta, String key) {
|
||||
long before = value(position.getHoldPackages());
|
||||
position.setHoldPackages(before + delta);
|
||||
position.setAvailablePackages(value(position.getAvailablePackages()) - delta);
|
||||
position.setVersion(value(position.getVersion()) + 1); position.setUpdatedAt(LocalDateTime.now());
|
||||
positions.updateById(position);
|
||||
ledger(identity, operation, balance, position, key, before, delta, before + delta,
|
||||
position.getPalletNo(), position.getPalletNo(), "现场按原入仓单确认拆托来源:冻结桶调整,可用桶反向调整,实物总数不变");
|
||||
}
|
||||
|
||||
private InventoryPosition moveInventory(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
PalletInventoryRow sourceRow, PlannedMove move, String key, String reason) {
|
||||
InventoryPosition source = positions.selectById(sourceRow.getPositionId());
|
||||
@@ -458,6 +570,89 @@ public class PdaPalletOperationService {
|
||||
if (destinations.size() > 1) throw conflict("不同目的仓货物不能组到同一托盘");
|
||||
}
|
||||
|
||||
@jakarta.annotation.Resource
|
||||
private com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper relabelWorkTasks;
|
||||
@jakarta.annotation.Resource
|
||||
private com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelUnitFactMapper relabelWorkFacts;
|
||||
@jakarta.annotation.Resource
|
||||
private com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper relabelWorkHistory;
|
||||
|
||||
private Set<String> boundRelabelMarks(RelabelApplication app) {
|
||||
var task = relabelWorkTasks.find(app.getCustomerId(), app.getId());
|
||||
if (task == null) return Set.of();
|
||||
String round = Objects.toString(task.getReviewedAt(), "INITIAL");
|
||||
Set<String> marks = new HashSet<>();
|
||||
var records = relabelWorkHistory.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory>()
|
||||
.eq("customer_id", app.getCustomerId()).eq("task_id", task.getId()).eq("action", "PALLET_BIND"));
|
||||
for (var record : records) {
|
||||
try {
|
||||
var data = objectMapper.readTree(record.getDetailJson());
|
||||
if (round.equals(data.path("round").asText())) data.path("cartonMarks").forEach(mark -> marks.add(mark.asText()));
|
||||
} catch (JsonProcessingException failure) { throw conflict("换标组托记录无法读取,请核对作业历史"); }
|
||||
}
|
||||
return marks;
|
||||
}
|
||||
|
||||
private void recordRelabelBinding(PdaWarehouseIdentity identity, RelabelMutation mutation,
|
||||
Map<Long, InventoryPosition> targets) {
|
||||
var app = mutation.application();
|
||||
var task = relabelWorkTasks.find(app.getCustomerId(), app.getId());
|
||||
if (task == null) throw conflict("换标作业任务不存在,不能记录组托");
|
||||
var record = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
|
||||
record.setId(IdWorker.getId()); record.setTaskId(task.getId()); record.setApplicationId(app.getId());
|
||||
record.setCustomerId(app.getCustomerId()); record.setAction("PALLET_BIND");
|
||||
record.setFromStatus(task.getStatus()); record.setToStatus(task.getStatus());
|
||||
record.setOperatorKey(identity.operatorKey()); record.setOperatorName(identity.operatorName());
|
||||
record.setCreatedAt(LocalDateTime.now());
|
||||
record.setDetailJson(write(Map.of("round", Objects.toString(task.getReviewedAt(), "INITIAL"),
|
||||
"cartonMarks", mutation.itemsByPosition().values().stream().flatMap(List::stream).map(RelabelItem::getCartonMark).toList(),
|
||||
"targetPallets", targets.values().stream().map(InventoryPosition::getPalletNo).distinct().toList())));
|
||||
relabelWorkHistory.insert(record);
|
||||
}
|
||||
|
||||
private void requireRelabelWork(PdaWarehouseIdentity identity, RelabelApplication application) {
|
||||
if (application == null || !Set.of("PROCESSING", "READY_FOR_OUTBOUND").contains(application.getStatus()))
|
||||
throw conflict("当前换标状态不能组托");
|
||||
if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId())))
|
||||
throw conflict("换标单不属于当前海外仓");
|
||||
requireUnallocatedRelabel(application);
|
||||
if ("PROCESSING".equals(application.getStatus())) {
|
||||
var task = relabelWorkTasks.find(application.getCustomerId(), application.getId());
|
||||
if (task == null || !"PROCESSING".equals(task.getStatus()) || !identity.operatorKey().equals(task.getOperatorKey()))
|
||||
throw conflict("请先认领当前换标任务");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireUnallocatedRelabel(RelabelApplication application) {
|
||||
String fulfillment = application.getFulfillmentStatus();
|
||||
if (org.springframework.util.StringUtils.hasText(application.getOutboundOrderNo())
|
||||
|| (org.springframework.util.StringUtils.hasText(fulfillment)
|
||||
&& !Set.of("WAIT_RELABEL", "WAIT_PRE_DISPATCH").contains(fulfillment)))
|
||||
throw conflict("换标单已进入排车或出库流程,请先解除占用后再改绑托盘");
|
||||
}
|
||||
|
||||
private void requireRelabelDestination(long warehouseId, List<RelabelItem> selected,
|
||||
PalletPhysicalGateway.PalletReference target) {
|
||||
Set<String> destinations = new HashSet<>();
|
||||
selected.forEach(item -> destinations.add(text(item.getDestinationFbaWarehouseCode())));
|
||||
if (destinations.size() != 1 || destinations.contains("")) throw conflict("请按已维护的新目的仓分批组托");
|
||||
for (PalletInventoryRow row : mapper.content(String.valueOf(warehouseId), target.palletId())) {
|
||||
var bound = relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
|
||||
.eq("customer_id", row.getCustomerId()).eq("source_inventory_position_id", row.getPositionId()));
|
||||
long held = 0;
|
||||
for (RelabelItem item : bound) {
|
||||
var app = relabelApplications.selectById(item.getApplicationId());
|
||||
if (app != null && Set.of("PROCESSING", "CUSTOMER_CONFIRM_PENDING", "READY_FOR_OUTBOUND").contains(app.getStatus())) {
|
||||
held += value(item.getNotifiedPackageCount()); destinations.add(text(item.getDestinationFbaWarehouseCode()));
|
||||
}
|
||||
}
|
||||
if (value(row.getAvailablePackages()) > 0 || value(row.getAllocatedPackages()) > 0
|
||||
|| value(row.getPickedPackages()) > 0 || value(row.getHoldPackages()) > held)
|
||||
destinations.add(text(row.getDestinationWarehouseCode()));
|
||||
}
|
||||
if (destinations.size() != 1) throw conflict("目标托盘内货物与本次换标的新目的仓不同,不能合托");
|
||||
}
|
||||
|
||||
private Map<String, PalletPhysicalGateway.PalletReference> resolvePallets(long warehouseId,
|
||||
PalletOperationRequests.PalletExpectation first, List<PalletOperationRequests.PalletExpectation> rest) {
|
||||
List<PalletOperationRequests.PalletExpectation> values = new ArrayList<>(); values.add(first); values.addAll(rest);
|
||||
@@ -658,6 +853,7 @@ public class PdaPalletOperationService {
|
||||
List<PlannedMove> moves, List<PalletChange> changes,
|
||||
RelabelMutation relabelMutation) { }
|
||||
private record RelabelMutation(RelabelApplication application,
|
||||
Map<Long, List<RelabelItem>> itemsByPosition) { }
|
||||
Map<Long, List<RelabelItem>> itemsByPosition,
|
||||
Map<Long, InventoryPosition> anchors) { }
|
||||
private enum InventoryBucket { AVAILABLE, HOLD }
|
||||
}
|
||||
|
||||
+10
@@ -36,6 +36,16 @@ public class RelabelApplicationController {
|
||||
return ApiResponse.success(pdaService.returnForRework(operator(user), user.username(), relabelNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{relabelNo}/confirm-completion")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_STATUS + "')")
|
||||
public ApiResponse<RelabelResponses.Detail> confirmCompletion(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String relabelNo,
|
||||
@Valid @RequestBody RelabelRequests.CustomerConfirm request) {
|
||||
requirePlatform(user);
|
||||
String customerId = service.customerId(relabelNo);
|
||||
return ApiResponse.success(service.confirmCompletion(customerId, relabelNo, operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/{relabelNo}/review-task")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_LIST + "')")
|
||||
public ApiResponse<com.baiyue.lastleg.relabel.pda.dto.PdaRelabelResponses.Detail> reviewTask(
|
||||
|
||||
@@ -39,9 +39,9 @@ public final class RelabelRequests {
|
||||
}
|
||||
|
||||
public record Item(
|
||||
@NotNull Long sourceInventoryBalanceId,
|
||||
@NotNull Long sourceInventoryPositionId,
|
||||
@NotNull Long sourceInboundOrderId,
|
||||
Long sourceInventoryBalanceId,
|
||||
Long sourceInventoryPositionId,
|
||||
Long sourceInboundOrderId,
|
||||
@Size(max = 128) String skuCode,
|
||||
@Size(max = 128) String productImageFileId,
|
||||
@Size(max = 255) String productImageFileName,
|
||||
@@ -61,8 +61,22 @@ public final class RelabelRequests {
|
||||
@Size(max = 255) String fbaCartonMark,
|
||||
@Size(max = 128) String destinationFbaWarehouseCode,
|
||||
@Size(max = 32) String operationType,
|
||||
@Size(max = 100) List<@Valid SkuItem> skuItems
|
||||
@Size(max = 100) List<@Valid SkuItem> skuItems,
|
||||
Long returnCartonId
|
||||
) {
|
||||
public Item(Long sourceInventoryBalanceId, Long sourceInventoryPositionId, Long sourceInboundOrderId,
|
||||
String skuCode, String productImageFileId, String productImageFileName, String productImageUrl,
|
||||
String productNameZh, int totalPackageCount, int notifiedPackageCount, String shipmentNo,
|
||||
String trackingCode, String newSkuCode, String labelRequirement, String remark,
|
||||
String relabelFileId, String relabelFileName, String relabelFileUrl, String cartonMark,
|
||||
String fbaCartonMark, String destinationFbaWarehouseCode, String operationType,
|
||||
List<SkuItem> skuItems) {
|
||||
this(sourceInventoryBalanceId, sourceInventoryPositionId, sourceInboundOrderId, skuCode,
|
||||
productImageFileId, productImageFileName, productImageUrl, productNameZh, totalPackageCount,
|
||||
notifiedPackageCount, shipmentNo, trackingCode, newSkuCode, labelRequirement, remark,
|
||||
relabelFileId, relabelFileName, relabelFileUrl, cartonMark, fbaCartonMark,
|
||||
destinationFbaWarehouseCode, operationType, skuItems, null);
|
||||
}
|
||||
/** 保持既有箱唛换标调用方的二进制参数形态。 */
|
||||
public Item(Long sourceInventoryBalanceId, Long sourceInventoryPositionId, Long sourceInboundOrderId,
|
||||
String skuCode, String productImageFileId, String productImageFileName, String productImageUrl,
|
||||
@@ -74,7 +88,7 @@ public final class RelabelRequests {
|
||||
productImageFileId, productImageFileName, productImageUrl, productNameZh, totalPackageCount,
|
||||
notifiedPackageCount, shipmentNo, trackingCode, newSkuCode, labelRequirement, remark,
|
||||
relabelFileId, relabelFileName, relabelFileUrl, cartonMark, fbaCartonMark,
|
||||
destinationFbaWarehouseCode, "CARTON_LABEL", List.of());
|
||||
destinationFbaWarehouseCode, "CARTON_LABEL", List.of(), null);
|
||||
}
|
||||
public Item(String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
@@ -85,7 +99,7 @@ public final class RelabelRequests {
|
||||
this(101L, 201L, 301L, skuCode, productImageFileId, productImageFileName, productImageUrl,
|
||||
productNameZh, totalPackageCount, notifiedPackageCount, shipmentNo, trackingCode,
|
||||
newSkuCode, labelRequirement, remark, relabelFileId, relabelFileName, relabelFileUrl,
|
||||
cartonMark, fbaCartonMark, destinationFbaWarehouseCode, "CARTON_LABEL", List.of());
|
||||
cartonMark, fbaCartonMark, destinationFbaWarehouseCode, "CARTON_LABEL", List.of(), null);
|
||||
}
|
||||
public Item(String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
@@ -94,7 +108,7 @@ public final class RelabelRequests {
|
||||
this(101L, 201L, 301L, skuCode, productImageFileId, productImageFileName, productImageUrl, productNameZh,
|
||||
totalPackageCount, notifiedPackageCount, shipmentNo, trackingCode, newSkuCode,
|
||||
labelRequirement, null, relabelFileId, relabelFileName, relabelFileUrl, skuCode, shipmentNo, "ONT8",
|
||||
"CARTON_LABEL", List.of());
|
||||
"CARTON_LABEL", List.of(), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ public final class RelabelResponses {
|
||||
String inboundCode, String overseasWarehouseId, String overseasWarehouseCode,
|
||||
String overseasWarehouseName, String destinationFbaWarehouseCode,
|
||||
String destinationFbaWarehouseName, String skuCode, String productNameZh,
|
||||
long availablePackages, long version, List<InventoryPosition> positions) {}
|
||||
long availablePackages, long version, List<InventoryPosition> positions,
|
||||
String sourceType, Long returnCartonId, String cartonMark,
|
||||
List<SkuItem> skuItems) {}
|
||||
public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo,
|
||||
String relabelOrderNo, String orderType, String status, Integer packageCount,
|
||||
LocalDateTime createdAt) {}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public class HttpRelabelDocumentFileGateway implements RelabelDocumentFileGatewa
|
||||
@Resource private RestClient.Builder restClientBuilder;
|
||||
@Resource private LastlegRestClientFactory restClientFactory;
|
||||
@Resource private LastlegRestClientProperties restClientProperties;
|
||||
@Value("${baiyue.clients.file.url:http://127.0.0.1:9205}") private String baseUrl;
|
||||
@Value("${baiyue.clients.file.url:http://127.0.0.1:9206}") private String baseUrl;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
private RestClient client;
|
||||
|
||||
|
||||
+10
@@ -6,4 +6,14 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RelabelItemMapper extends BaseMapper<RelabelItem> {
|
||||
@org.apache.ibatis.annotations.Select("SELECT id FROM ll_inventory_balance WHERE id=#{balanceId} AND customer_id=#{customerId} FOR UPDATE")
|
||||
Long lockSourceBalance(@org.apache.ibatis.annotations.Param("customerId") String customerId,
|
||||
@org.apache.ibatis.annotations.Param("balanceId") Long balanceId);
|
||||
|
||||
@org.apache.ibatis.annotations.Select("SELECT a.relabel_no FROM ll_relabel_item i JOIN ll_relabel_application a ON a.id=i.application_id " +
|
||||
"WHERE i.customer_id=#{customerId} AND a.customer_id=#{customerId} AND i.source_inventory_balance_id=#{balanceId} " +
|
||||
"AND UPPER(TRIM(i.carton_mark))=UPPER(TRIM(#{mark})) AND a.status NOT IN ('CANCELLED','REJECTED') LIMIT 1")
|
||||
String findExistingCarton(@org.apache.ibatis.annotations.Param("customerId") String customerId,
|
||||
@org.apache.ibatis.annotations.Param("balanceId") Long balanceId,
|
||||
@org.apache.ibatis.annotations.Param("mark") String mark);
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.lastleg.relabel.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/** 换标申请业务状态。 */
|
||||
public enum RelabelApplicationStatus {
|
||||
PENDING_PROCESSING("待处理"),
|
||||
PROCESSING("处理中"),
|
||||
CUSTOMER_CONFIRM_PENDING("待客户确认"),
|
||||
READY_FOR_OUTBOUND("换标完成,待出库"),
|
||||
COMPLETED("已完成"),
|
||||
REJECTED("已拒绝"),
|
||||
CANCELLED("已取消");
|
||||
|
||||
private final String label;
|
||||
|
||||
RelabelApplicationStatus(String label) { this.label = label; }
|
||||
|
||||
public String code() { return name(); }
|
||||
public String label() { return label; }
|
||||
|
||||
public static RelabelApplicationStatus fromCode(String code) {
|
||||
return Arrays.stream(values()).filter(value -> value.name().equals(code)).findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unsupported relabel application status: " + code));
|
||||
}
|
||||
}
|
||||
+8
@@ -70,6 +70,14 @@ public class PdaRelabelController {
|
||||
identity.overseasWarehouseId(), relabelNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{relabelNo}/skus/scan")
|
||||
public ApiResponse<PdaRelabelResponses.Detail> scanSku(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @Valid @RequestBody PdaRelabelRequests.SkuScan request) {
|
||||
Identity identity = identity(user);
|
||||
return ApiResponse.success(service.scanSku(identity.operatorKey(), identity.operatorName(),
|
||||
identity.overseasWarehouseId(), relabelNo, request));
|
||||
}
|
||||
|
||||
@GetMapping("/{relabelNo}/documents")
|
||||
public ApiResponse<List<PdaRelabelService.Document>> documents(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo) {
|
||||
|
||||
+4
@@ -14,6 +14,10 @@ public final class PdaRelabelRequests {
|
||||
private PdaRelabelRequests() { }
|
||||
|
||||
public record Claim(@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
public record SkuScan(@NotNull @Positive Long itemId, @Min(0) int skuIndex,
|
||||
@NotBlank @Size(max = 255) String oldSku,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
public record CartonScan(@NotBlank @Size(max = 255) String cartonMark,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ public final class PdaRelabelResponses {
|
||||
String cartonMark, String fbaCartonMark, String operationType,
|
||||
List<SkuItem> skuItems,
|
||||
int expectedPackages, int relabelledPackages, int skippedPackages,
|
||||
int exceptionPackages, int cartonProcessedPackages, int skuProcessedPackages) { }
|
||||
int exceptionPackages, int cartonProcessedPackages, int skuProcessedPackages,
|
||||
String destinationFbaWarehouseCode) { }
|
||||
public record SkuItem(String skuCode, String newSkuCode, int quantity, String remark) { }
|
||||
|
||||
public record UnitFact(Long factId, Long itemId, String oldLabel, String newLabel,
|
||||
|
||||
+12
-2
@@ -15,8 +15,18 @@ public interface PdaRelabelTaskMapper extends BaseMapper<PdaRelabelTask> {
|
||||
@Select("SELECT * FROM ll_relabel_pda_task WHERE customer_id=#{customerId} AND application_id=#{applicationId} LIMIT 1 FOR UPDATE")
|
||||
PdaRelabelTask lock(@Param("customerId") String customerId, @Param("applicationId") Long applicationId);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_pda_task WHERE operator_key=#{operatorKey} AND status<>'COMPLETED' ORDER BY claimed_at ASC LIMIT 1 FOR UPDATE")
|
||||
PdaRelabelTask lockUnfinishedByOperator(@Param("operatorKey") String operatorKey);
|
||||
@Select("""
|
||||
SELECT task.*
|
||||
FROM ll_relabel_pda_task task
|
||||
JOIN ll_relabel_application application
|
||||
ON application.id=task.application_id AND application.customer_id=task.customer_id
|
||||
WHERE task.operator_key=#{operatorKey}
|
||||
AND task.status='PROCESSING'
|
||||
AND application.status='PROCESSING'
|
||||
ORDER BY task.claimed_at ASC
|
||||
LIMIT 1 FOR UPDATE
|
||||
""")
|
||||
PdaRelabelTask lockProcessingByOperator(@Param("operatorKey") String operatorKey);
|
||||
|
||||
@Update("""
|
||||
UPDATE ll_relabel_pda_task
|
||||
|
||||
+57
-4
@@ -51,6 +51,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PdaRelabelService {
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard bindingGuard;
|
||||
public static final String PROCESSING = "PROCESSING";
|
||||
public static final String WAIT_REVIEW = "WAIT_REVIEW";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
@@ -109,7 +110,7 @@ public class PdaRelabelService {
|
||||
throw conflict("Relabel application is not executable");
|
||||
}
|
||||
PdaRelabelTask task = tasks.lock(application.getCustomerId(), application.getId());
|
||||
PdaRelabelTask activeTask = tasks.lockUnfinishedByOperator(operatorKey);
|
||||
PdaRelabelTask activeTask = tasks.lockProcessingByOperator(operatorKey);
|
||||
if (activeTask != null && !Objects.equals(activeTask.getApplicationId(), application.getId())) {
|
||||
throw conflict("当前账号正在执行换标单 " + activeTask.getRelabelNo() + ",请先完成后再操作其他换标单");
|
||||
}
|
||||
@@ -194,6 +195,49 @@ public class PdaRelabelService {
|
||||
|
||||
public record Document(String scope, String fileId, String fileName, Long itemId) { }
|
||||
|
||||
@Transactional
|
||||
public PdaRelabelResponses.Detail scanSku(String operatorKey, String operatorName, long warehouseId,
|
||||
String relabelNo, PdaRelabelRequests.SkuScan request) {
|
||||
UnitCommand command = new UnitCommand(relabelNo, "SCAN_SKU", request);
|
||||
var replayed = replay(operatorKey, warehouseId, relabelNo, "SCAN_SKU", request.idempotencyKey(), command);
|
||||
if (replayed != null) return replayed;
|
||||
var app = requireApplication(relabelNo, warehouseId, true);
|
||||
var task = requireOwnedTask(app, operatorKey, true);
|
||||
requireVersion(task, request.expectedVersion());
|
||||
var item = requireItem(app, request.itemId());
|
||||
if (!Set.of("SKU_LABEL", "CARTON_AND_SKU").contains(operationType(item))) throw conflict("该箱没有SKU换标任务");
|
||||
if (value(item.getNotifiedPackageCount()) != 1) throw conflict("SKU逐条作业要求每个仓库唛头独立一箱");
|
||||
var skus = readSkuItems(item.getSkuRelabelItemsJson());
|
||||
if (request.skuIndex() < 0 || request.skuIndex() >= skus.size()) throw conflict("SKU明细不存在");
|
||||
var sku = skus.get(request.skuIndex());
|
||||
if (!Objects.equals(sku.skuCode(), request.oldSku().trim()) || !StringUtils.hasText(sku.newSkuCode()) || sku.quantity() <= 0)
|
||||
throw conflict("旧SKU或绑定的新标签、数量不正确");
|
||||
if (factList(task).stream().anyMatch(fact -> item.getId().equals(fact.getItemId()))
|
||||
|| mixedStageDone(task, item.getId(), "SKU")) throw conflict("本箱SKU作业已完成");
|
||||
Set<Integer> done = new java.util.HashSet<>();
|
||||
for (var history : histories.selectList(new LambdaQueryWrapper<PdaRelabelHistory>()
|
||||
.eq(PdaRelabelHistory::getTaskId, task.getId()).eq(PdaRelabelHistory::getCustomerId, app.getCustomerId())
|
||||
.eq(PdaRelabelHistory::getAction, "SKU_SCAN"))) {
|
||||
if (!"SKU_SCAN".equals(history.getAction())) continue;
|
||||
try {
|
||||
var data = objectMapper.readTree(history.getDetailJson());
|
||||
if (mixedRound(task).equals(data.path("round").asText()) && item.getId().toString().equals(data.path("itemId").asText()))
|
||||
done.add(data.path("skuIndex").asInt(-1));
|
||||
} catch (Exception failure) { throw conflict("SKU扫描历史无法读取"); }
|
||||
}
|
||||
if (!done.add(request.skuIndex())) throw conflict("该SKU明细已处理,请勿重复扫描");
|
||||
insertHistory(task, "SKU_SCAN", PROCESSING, PROCESSING, operatorKey, operatorName,
|
||||
Map.of("round", mixedRound(task), "itemId", item.getId().toString(), "skuIndex", request.skuIndex(),
|
||||
"oldSku", sku.skuCode(), "newSku", sku.newSkuCode(), "quantity", sku.quantity()));
|
||||
if (done.size() == skus.size()) return unitAction(operatorKey, operatorName, warehouseId, relabelNo,
|
||||
"SCAN_SKU", request.idempotencyKey(), request, item.getId(), item.getCartonMark(), null,
|
||||
"RELABELLED", null, null, request.expectedVersion(), ignored -> { });
|
||||
updateTask(task, request.expectedVersion());
|
||||
var result = detail(app, task);
|
||||
saveOperation(task, operatorKey, "SCAN_SKU", request.idempotencyKey(), command, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Document> documents(long warehouseId, String relabelNo) {
|
||||
RelabelApplication app = requireApplication(relabelNo, warehouseId, false);
|
||||
@@ -264,6 +308,7 @@ public class PdaRelabelService {
|
||||
RelabelApplication application = requireApplication(relabelNo, overseasWarehouseId, true);
|
||||
PdaRelabelTask task = requireOwnedTask(application, operatorKey, true);
|
||||
requireVersion(task, request.expectedVersion());
|
||||
if (!request.manualCompletion()) bindingGuard.requireBound(application);
|
||||
List<RelabelItem> detailItems = itemList(application);
|
||||
List<PdaRelabelUnitFact> facts = factList(task);
|
||||
boolean incomplete = detailItems.stream().anyMatch(item -> facts.stream()
|
||||
@@ -558,7 +603,7 @@ public class PdaRelabelService {
|
||||
private PdaRelabelResponses.Candidate candidate(RelabelApplication application, PdaRelabelTask task,
|
||||
String operatorKey) {
|
||||
return new PdaRelabelResponses.Candidate(application.getRelabelNo(), application.getCustomerOrderNo(),
|
||||
application.getLabelDeadline(), application.getStatus(), task == null ? "AVAILABLE" : task.getStatus(),
|
||||
application.getLabelDeadline(), application.getStatus(), effectiveTaskStatus(application, task),
|
||||
operationType(application), value(application.getItemCount()),
|
||||
task == null ? value(application.getNotifiedPackageCount()) : value(task.getExpectedPackages()),
|
||||
task == null ? 0 : value(task.getRelabelledPackages()), task == null ? 0 : value(task.getSkippedPackages()),
|
||||
@@ -580,7 +625,8 @@ public class PdaRelabelService {
|
||||
value(item.getNotifiedPackageCount()), count(itemFacts, "RELABELLED"), count(itemFacts, "SKIPPED"),
|
||||
count(itemFacts, "EXCEPTION"),
|
||||
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "CARTON") ? 1 : 0),
|
||||
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "SKU") ? 1 : 0));
|
||||
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "SKU") ? 1 : 0),
|
||||
item.getDestinationFbaWarehouseCode());
|
||||
}).toList();
|
||||
List<PdaRelabelResponses.UnitFact> factResponses = facts.stream().map(fact ->
|
||||
new PdaRelabelResponses.UnitFact(fact.getId(), fact.getItemId(), fact.getOldLabel(), fact.getNewLabel(),
|
||||
@@ -593,7 +639,7 @@ public class PdaRelabelService {
|
||||
history.getToStatus(), history.getOperatorName(), history.getDetailJson(), history.getCreatedAt())).toList();
|
||||
return new PdaRelabelResponses.Detail(application.getRelabelNo(), application.getCustomerOrderNo(),
|
||||
application.getLabelDeadline(), application.getContactName(), application.getContactPhone(),
|
||||
application.getStatus(), operationType(application), task == null ? "AVAILABLE" : task.getStatus(),
|
||||
application.getStatus(), operationType(application), effectiveTaskStatus(application, task),
|
||||
task == null ? value(application.getNotifiedPackageCount()) : value(task.getExpectedPackages()),
|
||||
task == null ? 0 : value(task.getRelabelledPackages()), task == null ? 0 : value(task.getSkippedPackages()),
|
||||
task == null ? 0 : value(task.getExceptionPackages()), task == null ? 0 : value(task.getVersion()),
|
||||
@@ -605,6 +651,13 @@ public class PdaRelabelService {
|
||||
itemResponses, factResponses, historyResponses);
|
||||
}
|
||||
|
||||
private String effectiveTaskStatus(RelabelApplication application, PdaRelabelTask task) {
|
||||
if (task == null) return "AVAILABLE";
|
||||
if (RelabelApplicationService.CUSTOMER_CONFIRM_PENDING.equals(application.getStatus())
|
||||
&& PROCESSING.equals(task.getStatus())) return WAIT_REVIEW;
|
||||
return task.getStatus();
|
||||
}
|
||||
|
||||
private void insertHistory(PdaRelabelTask task, String action, String from, String to,
|
||||
String operatorKey, String operatorName, Object detail) {
|
||||
PdaRelabelHistory history = new PdaRelabelHistory();
|
||||
|
||||
+178
-44
@@ -29,6 +29,10 @@ import com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask;
|
||||
import com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPdaTask;
|
||||
@@ -84,6 +88,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private InventoryCommandService inventoryCommands;
|
||||
@Resource private InventoryBalanceMapper inventoryBalances;
|
||||
@Resource private InventoryPositionMapper inventoryPositions;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Resource private RelabelDocumentFileGateway documentFiles;
|
||||
@Resource private PdaRelabelTaskMapper pdaTasks;
|
||||
@Resource private RelabelPhotoFileGateway photoFiles;
|
||||
@@ -238,7 +244,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
.gt(InventoryBalance::getAvailablePackages, 0)
|
||||
.like(StringUtils.hasText(inboundCode), InventoryBalance::getInboundCode, trim(inboundCode))
|
||||
.orderByDesc(InventoryBalance::getUpdatedAt));
|
||||
return balances.stream().map(balance -> {
|
||||
List<RelabelResponses.InventoryCandidate> result = new java.util.ArrayList<>(balances.stream().map(balance -> {
|
||||
List<RelabelResponses.InventoryPosition> positions = inventoryPositions.listBalance(customerId, balance.getId())
|
||||
.stream().filter(position -> value(position.getAvailablePackages()) > 0)
|
||||
.map(position -> new RelabelResponses.InventoryPosition(position.getId(), position.getPalletNo(),
|
||||
@@ -249,8 +255,27 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
balance.getInboundOrderNo(), balance.getInboundCode(), balance.getOverseasWarehouseId(),
|
||||
balance.getOverseasWarehouseCode(), balance.getOverseasWarehouseName(),
|
||||
balance.getDestinationWarehouseCode(), balance.getDestinationWarehouseName(), balance.getSkuCode(),
|
||||
balance.getProductNameZh(), available, value(balance.getVersion()), positions);
|
||||
}).filter(candidate -> candidate.availablePackages() > 0).toList();
|
||||
balance.getProductNameZh(), available, value(balance.getVersion()), positions,
|
||||
"INBOUND", null, null, List.of());
|
||||
}).filter(candidate -> candidate.availablePackages() > 0).toList());
|
||||
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, operationWarehouseId.trim())) {
|
||||
String keyword = trim(inboundCode);
|
||||
if (StringUtils.hasText(keyword)
|
||||
&& !containsIgnoreCase(carton.getCartonMark(), keyword)
|
||||
&& !containsIgnoreCase(carton.getSourceReturnNo(), keyword)) continue;
|
||||
List<RelabelResponses.SkuItem> cartonSkus = returnCartonItems.listCarton(carton.getId()).stream()
|
||||
.filter(item -> value(item.getQuantity()) > 0)
|
||||
.map(item -> new RelabelResponses.SkuItem(item.getSkuCode(), null,
|
||||
Math.toIntExact(item.getQuantity()), null)).toList();
|
||||
if (cartonSkus.isEmpty()) continue;
|
||||
String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode)
|
||||
.collect(Collectors.joining(","));
|
||||
result.add(new RelabelResponses.InventoryCandidate(null, null, carton.getSourceReturnNo(),
|
||||
null, carton.getOverseasWarehouseId(), null, null, carton.getDestinationWarehouseCode(),
|
||||
carton.getDestinationWarehouseName(), skuCodes, "退货合箱库存", 1, 0, List.of(),
|
||||
"RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -280,6 +305,19 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
@Transactional
|
||||
public RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request) {
|
||||
return create(customerId, operatorId, request, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail createForExistingTailOrder(String customerId, String operatorId,
|
||||
Long existingTailOrderId,
|
||||
RelabelRequests.Create request) {
|
||||
if (existingTailOrderId == null) throw badRequest("Existing tail order is required");
|
||||
return create(customerId, operatorId, request, existingTailOrderId);
|
||||
}
|
||||
|
||||
private RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request,
|
||||
Long existingTailOrderId) {
|
||||
if (!StringUtils.hasText(request.operationWarehouseId())) throw badRequest("Operation warehouse is required");
|
||||
boolean requiresOutbound = request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
String deliveryMethod = trim(request.deliveryMethod());
|
||||
@@ -298,6 +336,13 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (!requestHash.equals(replay.getRequestHash())) throw conflict("Idempotency key was reused with different relabel data");
|
||||
return detail(customerId, replay.getRelabelNo());
|
||||
}
|
||||
Set<String> requestedDestinations = request.items().stream()
|
||||
.filter(item -> !"SKU_LABEL".equals(operationType(item)))
|
||||
.map(RelabelRequests.Item::destinationFbaWarehouseCode)
|
||||
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet());
|
||||
if (requestedDestinations.size() > 1) {
|
||||
throw badRequest("同一张贴标申请只能选择一个 FBA 目的仓");
|
||||
}
|
||||
int total = 0;
|
||||
int notified = 0;
|
||||
List<SourceInventory> sources = new java.util.ArrayList<>();
|
||||
@@ -312,6 +357,25 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (!selectedCartonMarks.add(item.cartonMark().trim().toUpperCase())) {
|
||||
throw badRequest("The same carton mark cannot create more than one relabel task");
|
||||
}
|
||||
if (item.returnCartonId() != null) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.returnCartonId());
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !request.operationWarehouseId().trim().equals(carton.getOverseasWarehouseId())) {
|
||||
throw conflict("所选退货箱不属于当前客户和操作仓");
|
||||
}
|
||||
if (!"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) {
|
||||
throw conflict("仓库唛头 " + carton.getCartonMark() + " 已不可用于换标");
|
||||
}
|
||||
if (!carton.getCartonMark().equalsIgnoreCase(item.cartonMark().trim())) {
|
||||
throw conflict("退货箱仓库唛头已变化,请重新查询");
|
||||
}
|
||||
sources.add(new SourceInventory(item, null, carton));
|
||||
continue;
|
||||
}
|
||||
if (item.sourceInventoryBalanceId() == null || item.sourceInventoryPositionId() == null
|
||||
|| item.sourceInboundOrderId() == null) {
|
||||
throw badRequest("正常入仓库存必须提供余额、仓位和入仓单");
|
||||
}
|
||||
InventoryPosition position = inventoryPositions.findTenantPosition(customerId, item.sourceInventoryPositionId());
|
||||
if (position == null || !Objects.equals(position.getBalanceId(), item.sourceInventoryBalanceId())) {
|
||||
throw conflict("Selected inventory position is no longer available");
|
||||
@@ -325,7 +389,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (value(position.getAvailablePackages()) < item.notifiedPackageCount()) {
|
||||
throw conflict("Available inventory changed for inbound " + balance.getInboundOrderNo());
|
||||
}
|
||||
sources.add(new SourceInventory(item, balance));
|
||||
sources.add(new SourceInventory(item, balance, null));
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long applicationId = IdWorker.getId();
|
||||
@@ -335,13 +399,15 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
boolean requiresWallet = chargeAmount.signum() > 0;
|
||||
int holdIndex = 0;
|
||||
for (SourceInventory source : sources) {
|
||||
if (source.returnCarton() != null) continue;
|
||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
source.request().sourceInventoryPositionId(), source.request().notifiedPackageCount(), relabelNo,
|
||||
null, "RELABEL:" + relabelNo, "RELABEL:" + applicationId + ":HOLD:" + holdIndex++,
|
||||
"贴标申请冻结原入仓单库存"));
|
||||
}
|
||||
LastlegOrder tailOrder = insertTailOrder(customerId, request, requestHash, relabelNo,
|
||||
operationWarehouseId, total, now);
|
||||
LastlegOrder tailOrder = existingTailOrderId == null
|
||||
? insertTailOrder(customerId, request, requestHash, relabelNo, operationWarehouseId, total, now)
|
||||
: linkExistingTailOrder(customerId, existingTailOrderId, relabelNo, total, now);
|
||||
List<LastlegOrder> tailOrders = List.of(tailOrder);
|
||||
|
||||
RelabelApplication application = new RelabelApplication();
|
||||
@@ -372,6 +438,14 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setUpdatedAt(now);
|
||||
applications.insert(application);
|
||||
|
||||
for (SourceInventory source : sources) {
|
||||
if (source.returnCarton() == null) continue;
|
||||
ReturnInventoryCarton carton = source.returnCarton();
|
||||
carton.setRelabelApplicationId(applicationId); carton.setRelabelNo(relabelNo);
|
||||
carton.setRelabelStatus("PENDING"); carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(now);
|
||||
returnCartons.updateById(carton);
|
||||
}
|
||||
|
||||
if (requiresWallet) {
|
||||
String walletEventId = walletCommands.publish("FREEZE", Long.parseLong(customerId), "RELABEL_ORDER",
|
||||
relabelNo, chargeAmount, "换标订单费用冻结", 1L);
|
||||
@@ -383,8 +457,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
insertItem(customerId, applicationId, source, now);
|
||||
}
|
||||
insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
|
||||
tailOrders.forEach(order -> insertOrderHistory(customerId, order.getId(), null,
|
||||
order.getCurrentStatus(), "Relabel application submitted", now));
|
||||
if (existingTailOrderId == null) {
|
||||
tailOrders.forEach(order -> insertOrderHistory(customerId, order.getId(), null,
|
||||
order.getCurrentStatus(), "Relabel application submitted", now));
|
||||
}
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@@ -501,7 +577,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
for (LastlegOrder order : linkedOrders) {
|
||||
String orderBefore = order.getCurrentStatus();
|
||||
String mainStatus = STATUSES.contains(orderBefore) ? "PENDING_ASSIGNMENT" : orderBefore;
|
||||
String mainStatus = "RELABEL_APPLICATION".equals(order.getOrderType())
|
||||
? "WAIT_RELABEL" : orderBefore;
|
||||
String orderStatus = switch (next) {
|
||||
case CUSTOMER_CONFIRM_PENDING -> mainStatus;
|
||||
case "COMPLETED" -> "RETURN_RELABEL".equals(order.getOrderType())
|
||||
@@ -606,23 +683,27 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
PdaRelabelTask reviewTask = pdaTasks.lock(customerId, application.getId());
|
||||
if (reviewTask == null || !Set.of("WAIT_REVIEW", "COMPLETED").contains(reviewTask.getStatus()))
|
||||
throw conflict("仓库尚未提交换标完工复核");
|
||||
reviewTask.setStatus("COMPLETED"); reviewTask.setReviewDecision("CUSTOMER_CONFIRMED");
|
||||
reviewTask.setReviewerKey(operatorId); reviewTask.setReviewerName("客户确认");
|
||||
reviewTask.setReviewRemark(request.remark()); reviewTask.setReviewedAt(LocalDateTime.now());
|
||||
reviewTask.setUpdatedAt(LocalDateTime.now());
|
||||
if (pdaTasks.updateWithVersion(reviewTask, reviewTask.getVersion()) != 1) throw conflict("复核状态已变化,请刷新");
|
||||
if (Boolean.TRUE.equals(application.getPrintShippingLabel())
|
||||
&& !StringUtils.hasText(application.getShippingLabelFileId())) {
|
||||
throw conflict("Upload the shipping label before confirming relabel completion");
|
||||
}
|
||||
List<RelabelItem> relabelItems = itemList(application.getId());
|
||||
boolean requiresOutbound = relabelItems.stream()
|
||||
.anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
reviewTask.setStatus("COMPLETED"); reviewTask.setReviewDecision("CONFIRMED");
|
||||
reviewTask.setReviewerKey(operatorId); reviewTask.setReviewerName("客户确认");
|
||||
reviewTask.setReviewRemark(request.remark()); reviewTask.setReviewedAt(LocalDateTime.now());
|
||||
reviewTask.setUpdatedAt(LocalDateTime.now());
|
||||
if (pdaTasks.updateWithVersion(reviewTask, reviewTask.getVersion()) != 1) throw conflict("复核状态已变化,请刷新");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String before = application.getStatus();
|
||||
application.setStatus(READY_FOR_OUTBOUND);
|
||||
application.setStatus(requiresOutbound ? READY_FOR_OUTBOUND : "COMPLETED");
|
||||
application.setCustomerConfirmationKey(key);
|
||||
application.setCustomerConfirmedAt(now);
|
||||
application.setFulfillmentStatus("TRUCK".equals(application.getDeliveryMethod())
|
||||
? "WAIT_PRE_DISPATCH" : "WAIT_OUTBOUND_CREATION");
|
||||
if (!"TRUCK".equals(application.getDeliveryMethod())) {
|
||||
application.setFulfillmentStatus(requiresOutbound
|
||||
? ("TRUCK".equals(application.getDeliveryMethod()) ? "WAIT_PRE_DISPATCH" : "WAIT_OUTBOUND_CREATION")
|
||||
: "NOT_REQUIRED");
|
||||
if (requiresOutbound && !"TRUCK".equals(application.getDeliveryMethod())) {
|
||||
createDirectOutbound(application, operatorId, now);
|
||||
application.setFulfillmentStatus("OUTBOUND_CREATED");
|
||||
}
|
||||
@@ -630,7 +711,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
applications.updateById(application);
|
||||
for (LastlegOrder order : tailOrders(application)) {
|
||||
String orderBefore = order.getCurrentStatus();
|
||||
order.setCurrentStatus("PENDING_ASSIGNMENT");
|
||||
order.setCurrentStatus(requiresOutbound ? "PENDING_ASSIGNMENT" : "COMPLETED");
|
||||
order.setSubOrderStatus("RELABEL:" + application.getFulfillmentStatus());
|
||||
order.setRelabelStatus("COMPLETED");
|
||||
order.setUpdatedAt(now);
|
||||
@@ -640,24 +721,34 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
request.remark(), now);
|
||||
}
|
||||
}
|
||||
insertHistory(customerId, application.getId(), before, READY_FOR_OUTBOUND, operatorId,
|
||||
insertHistory(customerId, application.getId(), before, application.getStatus(), operatorId,
|
||||
request.remark(), now);
|
||||
// 仓储转运换标在客户确认后立即恢复为可排车,发车时再转为 OUTBOUNDED。
|
||||
if (warehouseTransfers != null) {
|
||||
WarehouseTransferOrder transfer = warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
|
||||
if (transfer != null && "WAIT_RELABEL".equals(transfer.getStatus())) {
|
||||
transfer.setStatus("READY_FOR_DISPATCH");
|
||||
Set<String> destinations = relabelItems.stream()
|
||||
.map(RelabelItem::getDestinationFbaWarehouseCode)
|
||||
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet());
|
||||
if (!StringUtils.hasText(transfer.getFbaWarehouseCode()) && destinations.size() == 1) {
|
||||
transfer.setFbaWarehouseCode(destinations.iterator().next());
|
||||
}
|
||||
String transferStatus = StringUtils.hasText(transfer.getFbaWarehouseCode())
|
||||
? "READY_FOR_DISPATCH" : "WAIT_FBA_DESTINATION";
|
||||
transfer.setStatus(transferStatus);
|
||||
transfer.setRelabelCompletedAt(now);
|
||||
transfer.setUpdatedAt(now);
|
||||
warehouseTransfers.updateById(transfer);
|
||||
application.setFulfillmentStatus(transferStatus);
|
||||
applications.updateById(application);
|
||||
// 转运单自身的尾程订单与换标申请尾程订单是两条记录,需同步释放原转运单参与排车。
|
||||
LastlegOrder transferOrder = orders.selectById(transfer.getLastlegOrderId());
|
||||
if (transferOrder != null && customerId.equals(transferOrder.getCustomerId())) {
|
||||
String transferOrderBefore = transferOrder.getCurrentStatus();
|
||||
transferOrder.setCurrentStatus("READY_FOR_DISPATCH");
|
||||
transferOrder.setCurrentStatus(transferStatus);
|
||||
transferOrder.setFbaWarehouseCode(transfer.getFbaWarehouseCode());
|
||||
transferOrder.setRelabelStatus("COMPLETED");
|
||||
transferOrder.setSubOrderStatus("WAREHOUSE_TRANSFER:READY_FOR_DISPATCH;RELABEL:"
|
||||
+ "COMPLETED");
|
||||
transferOrder.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transferStatus + ";RELABEL:COMPLETED");
|
||||
transferOrder.setUpdatedAt(now);
|
||||
orders.updateById(transferOrder);
|
||||
if (!Objects.equals(transferOrderBefore, transferOrder.getCurrentStatus())) {
|
||||
@@ -683,27 +774,28 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RELABEL_OUTBOUND", application.getOverseasWarehouseId(), outbound.getId()));
|
||||
outbound.setOverseasWarehouseId(application.getOverseasWarehouseId()); outbound.setOutboundType("RELABEL_OUTBOUND");
|
||||
outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(relabelItems.stream().mapToInt(item -> intValue(item.getNotifiedPackageCount())).sum());
|
||||
outbound.setPalletCount((int) relabelItems.stream().map(RelabelItem::getSourceInventoryPositionId).filter(Objects::nonNull).distinct().count());
|
||||
outbound.setPalletCount((int) relabelItems.stream().map(this::outboundSourceKey).distinct().count());
|
||||
outbound.setRecipientInfo(application.getDeliveryMethod()); outbound.setIdempotencyKey(key);
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
|
||||
Map<Long, List<RelabelItem>> byPosition = relabelItems.stream().collect(Collectors.groupingBy(
|
||||
RelabelItem::getSourceInventoryPositionId, LinkedHashMap::new, Collectors.toList()));
|
||||
this::outboundSourceKey, LinkedHashMap::new, Collectors.toList()));
|
||||
for (Map.Entry<Long, List<RelabelItem>> entry : byPosition.entrySet()) {
|
||||
InventoryPosition position = inventoryPositions.selectById(entry.getKey());
|
||||
if (position == null || !application.getCustomerId().equals(position.getCustomerId())) {
|
||||
InventoryPosition position = entry.getKey() > 0 ? inventoryPositions.selectById(entry.getKey()) : null;
|
||||
if (entry.getKey() > 0 && (position == null || !application.getCustomerId().equals(position.getCustomerId()))) {
|
||||
throw conflict("Relabel source inventory position no longer exists");
|
||||
}
|
||||
List<RelabelItem> group = entry.getValue();
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId());
|
||||
item.setCustomerId(application.getCustomerId()); item.setOutboundOrderId(outbound.getId());
|
||||
item.setWarehousePalletId(position.getPalletId()); item.setPalletNo(position.getPalletNo());
|
||||
item.setWarehousePalletId(position == null ? null : position.getPalletId());
|
||||
item.setPalletNo(position == null ? null : position.getPalletNo());
|
||||
item.setPackageCount(group.stream().mapToInt(value -> intValue(value.getNotifiedPackageCount())).sum());
|
||||
item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setCartonMarks(group.stream().map(RelabelItem::getCartonMark).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setDestinationWarehouseCode(group.stream().map(RelabelItem::getDestinationFbaWarehouseCode)
|
||||
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setInboundCode(group.stream().map(RelabelItem::getSourceInboundCode).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setOrderRefs(writeOutboundRefs(application.getRelabelNo(), entry.getKey(), group));
|
||||
item.setOrderRefs(writeOutboundRefs(application.getRelabelNo(), position == null ? null : entry.getKey(), group));
|
||||
item.setCreatedAt(now); outboundItems.insert(item);
|
||||
}
|
||||
OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(application.getCustomerId());
|
||||
@@ -714,11 +806,14 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
private String writeOutboundRefs(String relabelNo, Long positionId, List<RelabelItem> values) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(Map.of("sourceType", "RELABEL_ORDER", "relabelNo", relabelNo,
|
||||
"sourceInventoryPositionId", positionId, "items", values.stream().map(item -> Map.of(
|
||||
Map<String, Object> refs = new LinkedHashMap<>();
|
||||
refs.put("sourceType", "RELABEL_ORDER"); refs.put("relabelNo", relabelNo);
|
||||
if (positionId != null) refs.put("sourceInventoryPositionId", positionId);
|
||||
refs.put("items", values.stream().map(item -> Map.of(
|
||||
"itemId", item.getId(), "cartonMark", Objects.toString(item.getCartonMark(), ""),
|
||||
"destination", Objects.toString(item.getDestinationFbaWarehouseCode(), ""),
|
||||
"packages", intValue(item.getNotifiedPackageCount()))).toList()));
|
||||
"packages", intValue(item.getNotifiedPackageCount()))).toList());
|
||||
return objectMapper.writeValueAsString(refs);
|
||||
} catch (Exception exception) { throw new IllegalStateException("Cannot save relabel outbound snapshot", exception); }
|
||||
}
|
||||
|
||||
@@ -740,14 +835,21 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
item.setDestinationFbaWarehouseCode(trim(source.destinationFbaWarehouseCode()));
|
||||
item.setOperationType(operationType(source));
|
||||
item.setSkuRelabelItemsJson(writeSkuItems(source.skuItems()));
|
||||
item.setSourceInventoryBalanceId(balance.getId());
|
||||
item.setSourceInventoryPositionId(source.sourceInventoryPositionId());
|
||||
item.setSourceInboundOrderId(balance.getInboundOrderId());
|
||||
item.setSourceInboundOrderNo(balance.getInboundOrderNo());
|
||||
item.setSourceInboundCode(balance.getInboundCode());
|
||||
item.setSourceWarehouseId(balance.getOverseasWarehouseId());
|
||||
item.setSourceWarehouseCode(balance.getOverseasWarehouseCode());
|
||||
item.setSourceWarehouseName(balance.getOverseasWarehouseName());
|
||||
if (balance != null) {
|
||||
item.setSourceInventoryBalanceId(balance.getId());
|
||||
item.setSourceInventoryPositionId(source.sourceInventoryPositionId());
|
||||
item.setSourceInboundOrderId(balance.getInboundOrderId());
|
||||
item.setSourceInboundOrderNo(balance.getInboundOrderNo());
|
||||
item.setSourceInboundCode(balance.getInboundCode());
|
||||
item.setSourceWarehouseId(balance.getOverseasWarehouseId());
|
||||
item.setSourceWarehouseCode(balance.getOverseasWarehouseCode());
|
||||
item.setSourceWarehouseName(balance.getOverseasWarehouseName());
|
||||
} else {
|
||||
ReturnInventoryCarton carton = sourceInventory.returnCarton();
|
||||
item.setReturnCartonId(carton.getId()); item.setSourceCartonMark(carton.getCartonMark());
|
||||
item.setSourceInboundOrderNo(carton.getSourceReturnNo());
|
||||
item.setSourceWarehouseId(carton.getOverseasWarehouseId());
|
||||
}
|
||||
item.setCreatedAt(now); items.insert(item);
|
||||
}
|
||||
|
||||
@@ -831,7 +933,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
order.setSourceInboundId(relabelNo + ":" + warehouseId);
|
||||
order.setSourceOrderNo(request.customerOrderNo().trim()); order.setOverseasWarehouseId(warehouseId);
|
||||
order.setPalletCount(0); order.setPackageCount(packageCount); order.setPickedPackageCount(0);
|
||||
order.setShippedPackageCount(0); order.setCurrentStatus("PENDING_ASSIGNMENT");
|
||||
order.setShippedPackageCount(0); order.setCurrentStatus("WAIT_RELABEL");
|
||||
order.setSubOrderStatus("RELABEL:" + PENDING_PROCESSING); order.setRelabelOrderNo(relabelNo);
|
||||
order.setRelabelStatus(PENDING_PROCESSING); order.setCustomerRemark(trim(request.remark()));
|
||||
order.setSourceEventId("CUSTOMER_RELABEL:" + relabelNo + ":" + warehouseId);
|
||||
@@ -840,6 +942,17 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return order;
|
||||
}
|
||||
|
||||
private LastlegOrder linkExistingTailOrder(String customerId, Long orderId, String relabelNo,
|
||||
int packageCount, LocalDateTime now) {
|
||||
LastlegOrder order = orders.selectById(orderId);
|
||||
if (order == null || !customerId.equals(order.getCustomerId())) {
|
||||
throw conflict("Existing tail order does not belong to this customer");
|
||||
}
|
||||
order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(PENDING_PROCESSING);
|
||||
order.setPackageCount(packageCount); order.setUpdatedAt(now); orders.updateById(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
private List<LastlegOrder> tailOrders(RelabelApplication application) {
|
||||
List<LastlegOrder> linked = orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, application.getCustomerId())
|
||||
@@ -850,12 +963,27 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return legacy == null ? List.of() : List.of(legacy);
|
||||
}
|
||||
|
||||
private record SourceInventory(RelabelRequests.Item request, InventoryBalance balance) {}
|
||||
private record SourceInventory(RelabelRequests.Item request, InventoryBalance balance,
|
||||
ReturnInventoryCarton returnCarton) {}
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private static int intValue(Integer value) { return value == null ? 0 : value; }
|
||||
private static boolean containsIgnoreCase(String value, String fragment) {
|
||||
return value != null && fragment != null
|
||||
&& value.toLowerCase(java.util.Locale.ROOT).contains(fragment.toLowerCase(java.util.Locale.ROOT));
|
||||
}
|
||||
|
||||
private void releaseInventory(String customerId, String operatorId, RelabelApplication application) {
|
||||
for (RelabelItem item : itemList(application.getId())) {
|
||||
if (item.getReturnCartonId() != null) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton != null && customerId.equals(carton.getCustomerId())
|
||||
&& Objects.equals(application.getId(), carton.getRelabelApplicationId())
|
||||
&& "RELABEL_PENDING".equals(carton.getStatus())) {
|
||||
carton.setRelabelApplicationId(null); carton.setRelabelNo(null); carton.setRelabelStatus(null);
|
||||
carton.setStatus("AVAILABLE"); carton.setUpdatedAt(LocalDateTime.now()); returnCartons.updateById(carton);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null) continue;
|
||||
inventoryCommands.release(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), application.getRelabelNo(), null,
|
||||
@@ -938,9 +1066,15 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
private static String businessRelabelStatus(String workflowStatus) {
|
||||
if ("PROCESSING".equals(workflowStatus)) return "PROCESSING";
|
||||
return Set.of(READY_FOR_OUTBOUND, "COMPLETED").contains(workflowStatus)
|
||||
return Set.of(CUSTOMER_CONFIRM_PENDING, READY_FOR_OUTBOUND, "COMPLETED").contains(workflowStatus)
|
||||
? "COMPLETED" : PENDING_PROCESSING;
|
||||
}
|
||||
|
||||
private long outboundSourceKey(RelabelItem item) {
|
||||
if (item.getSourceInventoryPositionId() != null) return item.getSourceInventoryPositionId();
|
||||
if (item.getReturnCartonId() != null) return -item.getReturnCartonId();
|
||||
throw conflict("Relabel outbound item has no physical inventory source");
|
||||
}
|
||||
private String hash(Object value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(objectMapper.copy().findAndRegisterModules().writeValueAsString(value).getBytes(StandardCharsets.UTF_8))); }
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.*;
|
||||
|
||||
/** Requires saved physical bindings, never the original accounting freeze anchor. */
|
||||
@Service
|
||||
public class RelabelBindingGuard {
|
||||
@Resource private RelabelItemMapper items;
|
||||
@Resource private PdaRelabelHistoryMapper histories;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
public void requireBound(RelabelApplication app) {
|
||||
List<RelabelItem> cartons = items.selectList(new QueryWrapper<RelabelItem>()
|
||||
.eq("application_id", app.getId()).eq("customer_id", app.getCustomerId()))
|
||||
.stream().filter(item -> !"SKU_LABEL".equals(item.getOperationType())).toList();
|
||||
if (cartons.isEmpty()) return;
|
||||
Map<String, Set<String>> bindings = new HashMap<>();
|
||||
for (PdaRelabelHistory history : histories.selectList(new QueryWrapper<PdaRelabelHistory>()
|
||||
.eq("application_id", app.getId()).eq("customer_id", app.getCustomerId())
|
||||
.orderByAsc("created_at", "id"))) {
|
||||
if ("REWORK_ARCHIVE".equals(history.getAction())) bindings.clear();
|
||||
if (!"PALLET_BIND".equals(history.getAction())) continue;
|
||||
try {
|
||||
var data = objectMapper.readTree(history.getDetailJson());
|
||||
Set<String> targets = new HashSet<>();
|
||||
data.path("targetPallets").forEach(value -> targets.add(value.asText()));
|
||||
data.path("cartonMarks").forEach(value -> bindings.put(value.asText(), targets));
|
||||
} catch (Exception e) { throw conflict("组托记录无法读取,请核对后重试"); }
|
||||
}
|
||||
for (RelabelItem item : cartons) {
|
||||
var position = item.getSourceInventoryPositionId() == null ? null
|
||||
: positions.findTenantPosition(app.getCustomerId(), item.getSourceInventoryPositionId());
|
||||
if (position == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(position.getPalletNo())
|
||||
|| position.getHoldPackages() == null || position.getHoldPackages() < item.getNotifiedPackageCount())
|
||||
throw conflict("箱唛 " + item.getCartonMark() + " 尚未完成托盘绑定,请先在 PDA 确认组托,不能完成或排车出库");
|
||||
}
|
||||
}
|
||||
private ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
@@ -569,7 +569,7 @@ public class LastlegOrderService {
|
||||
|
||||
public void writeExportCsv(String customerId, LastlegOrderRequests.Query query, OutputStream output)
|
||||
throws IOException {
|
||||
output.write("\uFEFFID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜节点,是否异常,是否暂扣,托数,箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
output.write("\uFEFFID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜节点,是否异常,是否暂扣,托数,客户预报箱数,实收箱数,差异箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
final int batchSize = 1000;
|
||||
long page = 1;
|
||||
while (true) {
|
||||
@@ -585,7 +585,8 @@ public class LastlegOrderService {
|
||||
csv(order.getTransferWarehouseName()), csv(order.getTransferWarehouseCode()), csv(order.getFbaWarehouseCode()),
|
||||
csv(order.getContainerNo()), csv(StringUtils.hasText(order.getSourceContainerBookingId())
|
||||
? containerNodes.get(order.getSourceContainerBookingId()) : null), csv(order.getAbnormal()), csv(order.getDetained()),
|
||||
csv(order.getPalletCount()), csv(order.getPackageCount()), csv(order.getVolumeCbm()), csv(order.getProductQuantity()),
|
||||
csv(order.getPalletCount()), csv(order.getPackageCount()), csv(order.getReceivedPackageCount()),
|
||||
csv(packageDifference(order)), csv(order.getVolumeCbm()), csv(order.getProductQuantity()),
|
||||
csv(order.getVolumeWeightKg()), csv(order.getEstimatedArrivalAt()), csv(order.getActualArrivalAt()),
|
||||
csv(order.getEstimatedWarehouseAt()), csv(order.getInboundOrderNo()), csv(order.getRelabelOrderNo()), csv(relabelBusinessStatus(order.getRelabelStatus())),
|
||||
csv(order.getPalletMergeFlag()), csv(order.getCurrentStatus()), csv(order.getSubOrderStatus()), csv(order.getPrivateRemark()),
|
||||
@@ -677,7 +678,8 @@ public class LastlegOrderService {
|
||||
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(), containerNode,
|
||||
o.getAbnormal(), o.getDetained(), o.getHoldChargeable(), o.getHoldReason(),
|
||||
Boolean.TRUE.equals(o.getManualNodeOverride()), Boolean.TRUE.equals(o.getManualNodeOverride()) ? "MANUAL" : "CONTAINER",
|
||||
o.getVersion() == null ? 0 : o.getVersion(), o.getPalletCount(), o.getPackageCount(), o.getVolumeCbm(),
|
||||
o.getVersion() == null ? 0 : o.getVersion(), o.getPalletCount(), o.getPackageCount(),
|
||||
o.getReceivedPackageCount(), packageDifference(o), o.getVolumeCbm(),
|
||||
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
|
||||
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), dispatchStatus, o.getRelabelOrderNo(), relabelBusinessStatus(o.getRelabelStatus()),
|
||||
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
|
||||
@@ -686,6 +688,11 @@ public class LastlegOrderService {
|
||||
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
|
||||
}
|
||||
|
||||
private static Integer packageDifference(LastlegOrder order) {
|
||||
if (order.getReceivedPackageCount() == null) return null;
|
||||
return order.getReceivedPackageCount() - java.util.Objects.requireNonNullElse(order.getPackageCount(), 0);
|
||||
}
|
||||
|
||||
static String displayOrderNo(LastlegOrder order) {
|
||||
return "WAREHOUSE_TRANSFER".equals(order.getOrderType()) && StringUtils.hasText(order.getSourceOrderNo())
|
||||
? order.getSourceOrderNo() : order.getOrderNo();
|
||||
|
||||
+55
@@ -14,6 +14,14 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/warehouse-transfers")
|
||||
@@ -27,6 +35,30 @@ public class MerchantWarehouseTransferController {
|
||||
return ApiResponse.success(service.create(customer(user), operator(user), request));
|
||||
}
|
||||
|
||||
@PutMapping("/{transferNo}")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> updateDraft(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
@Valid @RequestBody WarehouseTransferRequests.Create request) {
|
||||
return ApiResponse.success(service.updateDraft(customer(user), transferNo, operator(user), request));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 兼容入口:部分本地网关只放行 POST,草稿保存不能因此阻断提交。
|
||||
* 与 PUT 更新草稿使用完全相同的服务逻辑和租户校验。
|
||||
*/
|
||||
@PostMapping("/{transferNo}/save-draft")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> saveDraft(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
@Valid @RequestBody WarehouseTransferRequests.Create request) {
|
||||
return ApiResponse.success(service.updateDraft(customer(user), transferNo, operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/submit")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> submit(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String transferNo) {
|
||||
return ApiResponse.success(service.submit(customer(user), transferNo, operator(user)));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_LIST + "')")
|
||||
public ApiResponse<PageResult<WarehouseTransferResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@@ -41,6 +73,29 @@ public class MerchantWarehouseTransferController {
|
||||
return ApiResponse.success(service.detail(customer(user), transferNo));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}/carton-label")
|
||||
public ApiResponse<WarehouseTransferResponses.Label> cartonLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String transferNo) {
|
||||
return ApiResponse.success(service.cartonLabel(customer(user), transferNo));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}/measurement-photos/files")
|
||||
public ApiResponse<List<WarehouseTransferResponses.MeasurementPhoto>> measurementPhotos(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo) {
|
||||
return ApiResponse.success(service.measurementPhotos(customer(user), transferNo));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}/measurement-photos/files/{fileId}/download")
|
||||
public ResponseEntity<byte[]> downloadMeasurementPhoto(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String transferNo,
|
||||
@PathVariable long fileId) {
|
||||
var file = service.downloadMeasurementPhoto(customer(user), transferNo, fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition.inline().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(MediaType.parseMediaType(file.contentType())).body(file.content());
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/confirm-measurement")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CONFIRM + "')")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> confirmMeasurement(
|
||||
|
||||
+90
@@ -16,7 +16,16 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@@ -32,6 +41,42 @@ public class PdaWarehouseTransferController {
|
||||
return ApiResponse.success(service.pdaCandidates(identity.overseasWarehouseId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.pdaDetail(identity.overseasWarehouseId(), transferNo));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/{transferNo}/measurement-photos/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ApiResponse<WarehouseTransferResponses.MeasurementPhoto> uploadMeasurementPhoto(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
long operatorUserId = user.userId() == null ? 0L : user.userId();
|
||||
return ApiResponse.success(service.uploadMeasurementPhoto(identity.operatorKey(), operatorUserId,
|
||||
identity.overseasWarehouseId(), transferNo, file));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}/measurement-photos/files")
|
||||
public ApiResponse<List<WarehouseTransferResponses.MeasurementPhoto>> measurementPhotos(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.pdaMeasurementPhotos(identity.overseasWarehouseId(), transferNo));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}/measurement-photos/files/{fileId}/download")
|
||||
public ResponseEntity<byte[]> downloadMeasurementPhoto(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String transferNo,
|
||||
@PathVariable long fileId) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
var file = service.downloadPdaMeasurementPhoto(identity.overseasWarehouseId(), transferNo, fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition.inline().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(MediaType.parseMediaType(file.contentType())).body(file.content());
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/palletize")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> palletize(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
@@ -40,4 +85,49 @@ public class PdaWarehouseTransferController {
|
||||
return ApiResponse.success(service.palletize(identity.operatorKey(), identity.overseasWarehouseId(),
|
||||
transferNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/pallets/drafts")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> savePalletDraft(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
@Valid @RequestBody WarehouseTransferRequests.PalletDraft request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.savePalletDraft(identity.overseasWarehouseId(), transferNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/pallets/{palletId}/generate")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> generatePallet(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo, @PathVariable long palletId,
|
||||
@Valid @RequestBody WarehouseTransferRequests.PalletAction request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.generatePallet(identity.overseasWarehouseId(), transferNo, palletId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/pallets/{palletId}/bind")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> bindPallet(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo, @PathVariable long palletId,
|
||||
@Valid @RequestBody WarehouseTransferRequests.PalletBind request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.bindPallet(identity.overseasWarehouseId(), transferNo, palletId, request));
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.DeleteMapping("/{transferNo}/pallets/{palletId}")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> deletePalletDraft(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo, @PathVariable long palletId) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.deletePalletDraft(identity.overseasWarehouseId(), transferNo, palletId));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}/pallets/{warehousePalletId}/label")
|
||||
public ResponseEntity<byte[]> palletLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String transferNo,
|
||||
@PathVariable long warehousePalletId,
|
||||
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
var file = service.printPdaPalletLabel(identity.overseasWarehouseId(), transferNo,
|
||||
warehousePalletId, requestId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition.attachment().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(MediaType.parseMediaType(file.contentType())).body(file.content());
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -24,14 +24,14 @@ public class WarehouseTransferController {
|
||||
@PostMapping
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @Valid @RequestBody WarehouseTransferRequests.Create request) {
|
||||
return ApiResponse.success(service.create(scope(user, customerId), operator(user), request));
|
||||
return ApiResponse.success(service.createAndSubmit(scope(user, customerId), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
public ApiResponse<PageResult<WarehouseTransferResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @Valid WarehouseTransferRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query, false));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}")
|
||||
@@ -56,6 +56,17 @@ public class WarehouseTransferController {
|
||||
return ApiResponse.success(service.review(scope(user, customerId), transferNo, operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/bind-destination")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_CREATE + "')")
|
||||
public ApiResponse<java.util.List<WarehouseTransferResponses.Detail>> bindDestination(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody WarehouseTransferRequests.BindDestination request) {
|
||||
if (user == null || user.tenantId() == null || user.tenantId() != 0L || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform identity is required");
|
||||
}
|
||||
return ApiResponse.success(service.bindDestination(operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/allocations")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> allocate(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String transferNo,
|
||||
|
||||
+53
-11
@@ -21,7 +21,17 @@ public final class WarehouseTransferRequests {
|
||||
@Size(max = 200) List<@NotNull Long> sourceLastlegOrderIds,
|
||||
@NotBlank @Size(max = 128) String operatingWarehouseId,
|
||||
@Size(max = 128) String fbaWarehouseCode,
|
||||
@Size(max = 16) String destinationType,
|
||||
@Size(max = 128) String destinationWarehouseId,
|
||||
@Size(max = 128) String destinationWarehouseCode,
|
||||
@Size(max = 255) String destinationWarehouseName,
|
||||
@Size(max = 1000) String privateDestinationAddress,
|
||||
@Size(max = 32) String privateDestinationPostalCode,
|
||||
@Size(max = 128) String privateDestinationContactName,
|
||||
@Size(max = 64) String privateDestinationContactPhone,
|
||||
@NotBlank @Size(max = 1000) String deliveryAddress,
|
||||
@Size(max = 128) String pickupContactName,
|
||||
@Size(max = 64) String pickupContactPhone,
|
||||
@Positive int packageCount,
|
||||
@NotNull @DecimalMin(value = "0.0") BigDecimal volumeCbm,
|
||||
@NotBlank @Size(max = 64) String deliveryMethod,
|
||||
@@ -46,12 +56,29 @@ public final class WarehouseTransferRequests {
|
||||
String deliveryMethod, LocalDateTime estimatedArrivalAt, String fbaShipmentNo,
|
||||
String trackingNo, String fbaBoxNos, String remark, boolean needRelabel,
|
||||
String idempotencyKey, List<Item> items) {
|
||||
this(inboundCode, sourceLastlegOrderIds, operatingWarehouseId, fbaWarehouseCode, deliveryAddress,
|
||||
packageCount, volumeCbm, deliveryMethod, estimatedArrivalAt, null, fbaShipmentNo, trackingNo,
|
||||
this(inboundCode, sourceLastlegOrderIds, operatingWarehouseId, fbaWarehouseCode,
|
||||
null, null, null, null, null, null, null, null,
|
||||
deliveryAddress, null, null, packageCount, volumeCbm, deliveryMethod, estimatedArrivalAt, null, fbaShipmentNo, trackingNo,
|
||||
fbaBoxNos, remark, needRelabel, null, null, null, null, null, idempotencyKey, items, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record DestinationSelection(
|
||||
@NotBlank @Size(max = 128) String customerId,
|
||||
@NotBlank @Size(max = 64) String transferNo) {}
|
||||
|
||||
public record BindDestination(
|
||||
@NotEmpty @Size(max = 100) List<@Valid DestinationSelection> transfers,
|
||||
@NotBlank @jakarta.validation.constraints.Pattern(regexp = "FBA|PRIVATE") String destinationType,
|
||||
@Size(max = 128) String destinationWarehouseId,
|
||||
@Size(max = 128) String destinationWarehouseCode,
|
||||
@Size(max = 255) String destinationWarehouseName,
|
||||
@Size(max = 1000) String privateDestinationAddress,
|
||||
@Size(max = 32) String privateDestinationPostalCode,
|
||||
@Size(max = 128) String privateDestinationContactName,
|
||||
@Size(max = 64) String privateDestinationContactPhone,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {}
|
||||
|
||||
/** 审核通过备注选填;驳回必须提供非空原因,由服务在任何状态写入前校验。 */
|
||||
public record Review(@NotBlank @Size(max = 32) String decision,
|
||||
@Size(max = 1000) String remark) {}
|
||||
@@ -72,19 +99,34 @@ public final class WarehouseTransferRequests {
|
||||
|
||||
public record Palletize(
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@NotEmpty @Size(max = 100) List<@Valid Pallet> pallets
|
||||
@NotEmpty @Size(max = 20) List<@NotNull Long> photoFileIds
|
||||
) {}
|
||||
|
||||
public record PalletDraft(
|
||||
Long palletId,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Positive int cartonCount,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal lengthCm,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal widthCm,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal heightCm,
|
||||
@NotNull @DecimalMin("0.000001") BigDecimal volumeCbm,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal grossWeightKg
|
||||
) {}
|
||||
|
||||
public record PalletAction(@NotBlank @Size(max = 255) String idempotencyKey) {}
|
||||
|
||||
public record PalletBind(@NotBlank @Size(max = 128) String palletCode,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {}
|
||||
|
||||
public record Pallet(
|
||||
@Size(max = 128) String palletCode,
|
||||
@Positive int cartonCount,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal lengthCm,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal widthCm,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal heightCm,
|
||||
@NotNull @DecimalMin("0.000001") BigDecimal volumeCbm,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal grossWeightKg,
|
||||
@NotEmpty @Size(max = 500) List<@Valid PalletItem> items
|
||||
@NotNull @DecimalMin("0.01") BigDecimal grossWeightKg
|
||||
) {}
|
||||
|
||||
public record PalletItem(@NotNull Long transferItemId, @Positive int packageCount) {}
|
||||
|
||||
public record CustomerConfirm(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
@@ -106,8 +148,8 @@ public final class WarehouseTransferRequests {
|
||||
) {}
|
||||
|
||||
public record Item(
|
||||
@NotBlank @Size(max = 128) String skuCode,
|
||||
@NotBlank @Size(max = 255) String productNameZh,
|
||||
@Size(max = 128) String skuCode,
|
||||
@Size(max = 255) String productNameZh,
|
||||
@Positive int packageCount,
|
||||
@Size(max = 255) String shippingMark
|
||||
) {}
|
||||
@@ -115,7 +157,7 @@ public final class WarehouseTransferRequests {
|
||||
public record RelabelItem(
|
||||
@NotBlank @Size(max = 32) String operationType,
|
||||
@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> customerCartonMarks,
|
||||
@NotBlank @Size(max = 128) String originalSku,
|
||||
@Size(max = 128) String originalSku,
|
||||
@Size(max = 128) String newSku,
|
||||
@Positive int quantity,
|
||||
@Size(max = 255) String fbaCartonMark,
|
||||
|
||||
+17
-4
@@ -9,12 +9,16 @@ public final class WarehouseTransferResponses {
|
||||
public record Item(Long id, String skuCode, String productNameZh, int packageCount, String shippingMark) {}
|
||||
public record Carton(String cartonMark, String customerCartonMark, int cartonIndex, int totalCartons, String status) {}
|
||||
public record Label(String transferNo, List<Carton> cartons, String printableHtml) {}
|
||||
public record DestinationResult(String customerId, String transferNo) {}
|
||||
public record PalletItem(Long transferItemId, String skuCode, int packageCount) {}
|
||||
public record MeasurementPhoto(Long fileId, String fileName, String contentType, long sizeBytes,
|
||||
LocalDateTime uploadedAt) {}
|
||||
public record Pallet(Long id, int sequenceNo, Long warehousePalletId, String palletNo,
|
||||
Long warehouseLocationId, String warehouseLocationCode, int cartonCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
BigDecimal grossWeightKg, BigDecimal volumeCbm, String status,
|
||||
List<PalletItem> items, LocalDateTime completedAt) {}
|
||||
String fbaWarehouseCode, String labelDownloadPath,
|
||||
List<PalletItem> items, LocalDateTime boundAt, LocalDateTime completedAt) {}
|
||||
public record RelabelBinding(String operationType, String customerCartonMark, String originalSku,
|
||||
String newSku, int quantity, String fbaCartonMark,
|
||||
String destinationWarehouseCode, String remark) {}
|
||||
@@ -24,19 +28,28 @@ public final class WarehouseTransferResponses {
|
||||
Long destinationInventoryPositionId) {}
|
||||
public record Summary(Long id, String transferNo, String inboundCode, Long lastlegOrderId,
|
||||
String lastlegOrderNo, String operatingWarehouseId, String fbaWarehouseCode,
|
||||
String destinationType, String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int packageCount, String deliveryMethod, boolean needRelabel, String relabelNo,
|
||||
String status, LocalDateTime estimatedArrivalAt, LocalDateTime createdAt) {}
|
||||
public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode,
|
||||
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
|
||||
String status,
|
||||
boolean needRelabel, Long relabelApplicationId, String relabelNo, String relabelStatus,
|
||||
String operatingWarehouseId, String fbaWarehouseCode, String deliveryAddress,
|
||||
int packageCount, String deliveryMethod, LocalDateTime estimatedArrivalAt,
|
||||
String operatingWarehouseId, String fbaWarehouseCode, String deliveryAddress,
|
||||
String destinationType, String destinationWarehouseId, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String privateDestinationAddress,
|
||||
String privateDestinationPostalCode,
|
||||
String privateDestinationContactName, String privateDestinationContactPhone,
|
||||
String pickupContactName, String pickupContactPhone,
|
||||
int packageCount, BigDecimal volumeCbm, String deliveryMethod, LocalDateTime estimatedArrivalAt,
|
||||
LocalDateTime estimatedShipAt,
|
||||
String fbaShipmentNo, String trackingNo, String fbaBoxNos, String remark,
|
||||
LocalDateTime labelDeadline, String relabelContactName, String relabelContactPhone,
|
||||
String relabelDeliveryMethod,
|
||||
String reviewDecision, String reviewRemark, String reviewedBy, LocalDateTime reviewedAt,
|
||||
List<SourceOrder> sourceOrders, List<Item> items, List<Carton> cartons,
|
||||
List<Pallet> pallets, List<RelabelBinding> relabelBindings,
|
||||
List<Allocation> allocations, LocalDateTime customerConfirmedAt,
|
||||
List<Allocation> allocations, List<MeasurementPhoto> measurementPhotos,
|
||||
LocalDateTime customerConfirmedAt,
|
||||
LocalDateTime actualShipAt, LocalDateTime createdAt) {}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.transfer.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_warehouse_transfer_measurement_photo")
|
||||
public class WarehouseTransferMeasurementPhoto {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long transferOrderId;
|
||||
private String customerId;
|
||||
private Long fileId;
|
||||
private String fileName;
|
||||
private String contentType;
|
||||
private Long sizeBytes;
|
||||
private String uploadedBy;
|
||||
private LocalDateTime uploadedAt;
|
||||
private LocalDateTime submittedAt;
|
||||
}
|
||||
+12
@@ -18,7 +18,17 @@ public class WarehouseTransferOrder {
|
||||
private String lastlegOrderNo;
|
||||
private String operatingWarehouseId;
|
||||
private String fbaWarehouseCode;
|
||||
private String destinationType;
|
||||
private String destinationWarehouseId;
|
||||
private String destinationWarehouseCode;
|
||||
private String destinationWarehouseName;
|
||||
private String privateDestinationAddress;
|
||||
private String privateDestinationPostalCode;
|
||||
private String privateDestinationContactName;
|
||||
private String privateDestinationContactPhone;
|
||||
private String deliveryAddress;
|
||||
private String pickupContactName;
|
||||
private String pickupContactPhone;
|
||||
private Integer packageCount;
|
||||
private String deliveryMethod;
|
||||
private LocalDateTime estimatedArrivalAt;
|
||||
@@ -50,6 +60,8 @@ public class WarehouseTransferOrder {
|
||||
private String palletizeRequestHash;
|
||||
private String confirmationIdempotencyKey;
|
||||
private String confirmationRequestHash;
|
||||
private String destinationBindingIdempotencyKey;
|
||||
private String destinationBindingRequestHash;
|
||||
private LocalDateTime customerConfirmedAt;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
|
||||
+5
@@ -27,6 +27,11 @@ public class WarehouseTransferPallet {
|
||||
private BigDecimal grossWeightKg;
|
||||
private BigDecimal volumeCbm;
|
||||
private String status;
|
||||
private String draftIdempotencyKey;
|
||||
private String draftRequestHash;
|
||||
private String generateIdempotencyKey;
|
||||
private String bindIdempotencyKey;
|
||||
private LocalDateTime boundAt;
|
||||
private LocalDateTime completedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package com.baiyue.lastleg.transfer.gateway;
|
||||
|
||||
import com.baiyue.lastleg.config.LastlegRestClientFactory;
|
||||
import com.baiyue.lastleg.config.LastlegRestClientProperties;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class HttpTransferMeasurementPhotoFileGateway implements TransferMeasurementPhotoFileGateway {
|
||||
private static final String TYPE = "OVERSEAS_TRANSFER_MEASUREMENT_PHOTO";
|
||||
@Resource private RestClient.Builder restClientBuilder;
|
||||
@Resource private LastlegRestClientFactory restClientFactory;
|
||||
@Resource private LastlegRestClientProperties restClientProperties;
|
||||
@Value("${baiyue.clients.file.url:http://127.0.0.1:9206}") private String baseUrl;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
private RestClient client;
|
||||
|
||||
@PostConstruct void initializeClient() {
|
||||
client = restClientFactory.build(restClientBuilder, baseUrl, restClientProperties.getRelabelPhotoFile());
|
||||
}
|
||||
|
||||
@Override public UploadedFile upload(String customerId, long transferId, long operatorId, MultipartFile file) {
|
||||
MultipartBodyBuilder body = new MultipartBodyBuilder();
|
||||
try {
|
||||
body.part("file", new ByteArrayResource(file.getBytes()) {
|
||||
@Override public String getFilename() { return file.getOriginalFilename(); }
|
||||
}).contentType(file.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM
|
||||
: MediaType.parseMediaType(file.getContentType()));
|
||||
} catch (IOException exception) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot read transfer measurement photo", exception);
|
||||
}
|
||||
MetadataEnvelope response = client.post().uri("/internal/file/v1/business-files/{tenant}/{type}/{business}",
|
||||
tenant(customerId), TYPE, transferId)
|
||||
.header("X-Internal-Service-Token", internalToken).header("X-Operator-Id", String.valueOf(operatorId))
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA).body(body.build()).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw failure(result.getStatusCode(), "Transfer measurement photo upload failed"); })
|
||||
.body(MetadataEnvelope.class);
|
||||
Metadata data = success(response, "Transfer measurement photo upload failed");
|
||||
return new UploadedFile(data.fileId(), data.fileName(), data.contentType(), data.sizeBytes() == null ? 0 : data.sizeBytes());
|
||||
}
|
||||
|
||||
@Override public void validate(String customerId, long transferId, long fileId) {
|
||||
long tenant = tenant(customerId);
|
||||
MetadataEnvelope response = client.get().uri("/internal/file/v1/business-files/{tenant}/{type}/{business}/{file}",
|
||||
tenant, TYPE, transferId, fileId).header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw failure(result.getStatusCode(), "Transfer measurement photo ownership is invalid"); })
|
||||
.body(MetadataEnvelope.class);
|
||||
Metadata data = success(response, "Transfer measurement photo ownership is invalid");
|
||||
if (data.fileId() != fileId || data.tenantId() == null || data.tenantId() != tenant
|
||||
|| !TYPE.equals(data.businessType()) || !String.valueOf(transferId).equals(data.businessId())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Transfer measurement photo ownership is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@Override public DownloadedFile download(String customerId, long transferId, long fileId) {
|
||||
validate(customerId, transferId, fileId);
|
||||
ResponseEntity<byte[]> response = client.get().uri("/internal/file/v1/business-files/{tenant}/{type}/{business}/{file}/content",
|
||||
tenant(customerId), TYPE, transferId, fileId).header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw failure(result.getStatusCode(), "Transfer measurement photo download failed"); })
|
||||
.toEntity(byte[].class);
|
||||
String name = response.getHeaders().getContentDisposition().getFilename();
|
||||
String type = response.getHeaders().getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE
|
||||
: response.getHeaders().getContentType().toString();
|
||||
return new DownloadedFile(response.getBody() == null ? new byte[0] : response.getBody(),
|
||||
name == null ? "transfer-measurement-photo" : name, type);
|
||||
}
|
||||
|
||||
private static long tenant(String value) {
|
||||
try { return Long.parseLong(value); }
|
||||
catch (NumberFormatException exception) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Customer ID must be numeric", exception); }
|
||||
}
|
||||
private static Metadata success(MetadataEnvelope response, String fallback) {
|
||||
if (response == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, fallback);
|
||||
if (!"200".equals(response.code())) {
|
||||
HttpStatus status = switch (String.valueOf(response.code())) {
|
||||
case "A0400", "A0422" -> HttpStatus.BAD_REQUEST;
|
||||
case "A0403" -> HttpStatus.FORBIDDEN;
|
||||
case "A0404" -> HttpStatus.NOT_FOUND;
|
||||
default -> HttpStatus.SERVICE_UNAVAILABLE;
|
||||
};
|
||||
throw new ResponseStatusException(status,
|
||||
response.message() == null || response.message().isBlank() ? fallback : response.message());
|
||||
}
|
||||
if (response.data() == null || response.data().fileId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"File service returned no transfer measurement photo metadata");
|
||||
}
|
||||
return response.data();
|
||||
}
|
||||
private static ResponseStatusException failure(HttpStatusCode status, String message) {
|
||||
return new ResponseStatusException(status.is4xxClientError() ? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE, message);
|
||||
}
|
||||
private record MetadataEnvelope(String code, String message, Metadata data) {}
|
||||
private record Metadata(Long fileId, Long tenantId, String businessType, String businessId,
|
||||
String fileName, String contentType, Long sizeBytes) {}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.lastleg.transfer.gateway;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface TransferMeasurementPhotoFileGateway {
|
||||
UploadedFile upload(String customerId, long transferId, long operatorId, MultipartFile file);
|
||||
void validate(String customerId, long transferId, long fileId);
|
||||
DownloadedFile download(String customerId, long transferId, long fileId);
|
||||
|
||||
record UploadedFile(long fileId, String fileName, String contentType, long sizeBytes) {}
|
||||
record DownloadedFile(byte[] content, String fileName, String contentType) {}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.baiyue.lastleg.transfer.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferMeasurementPhoto;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface WarehouseTransferMeasurementPhotoMapper extends BaseMapper<WarehouseTransferMeasurementPhoto> {
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_measurement_photo WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY uploaded_at,id")
|
||||
List<WarehouseTransferMeasurementPhoto> listOrder(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_measurement_photo WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} AND submitted_at IS NOT NULL ORDER BY submitted_at,id")
|
||||
List<WarehouseTransferMeasurementPhoto> listSubmitted(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_measurement_photo WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} AND file_id=#{fileId} LIMIT 1")
|
||||
WarehouseTransferMeasurementPhoto findFile(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId,
|
||||
@Param("fileId") Long fileId);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_measurement_photo WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} AND file_id=#{fileId} AND submitted_at IS NOT NULL LIMIT 1")
|
||||
WarehouseTransferMeasurementPhoto findSubmittedFile(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId,
|
||||
@Param("fileId") Long fileId);
|
||||
@Update("<script>UPDATE ll_warehouse_transfer_measurement_photo SET submitted_at=#{submittedAt} WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} AND file_id IN <foreach collection='fileIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
|
||||
int markSubmitted(@Param("customerId") String customerId, @Param("transferOrderId") Long transferOrderId,
|
||||
@Param("fileIds") java.util.Collection<Long> fileIds,
|
||||
@Param("submittedAt") java.time.LocalDateTime submittedAt);
|
||||
}
|
||||
+2
@@ -14,6 +14,8 @@ import org.apache.ibatis.annotations.Update;
|
||||
WarehouseTransferOrder lockNo(@Param("customerId") String customerId, @Param("transferNo") String transferNo);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE transfer_no=#{transferNo} AND operating_warehouse_id=#{warehouseId} LIMIT 1 FOR UPDATE")
|
||||
WarehouseTransferOrder lockForPda(@Param("transferNo") String transferNo, @Param("warehouseId") String warehouseId);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE transfer_no=#{transferNo} AND operating_warehouse_id=#{warehouseId} LIMIT 1")
|
||||
WarehouseTransferOrder findNoByWarehouse(@Param("transferNo") String transferNo, @Param("warehouseId") String warehouseId);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
|
||||
WarehouseTransferOrder lockByRelabelNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE operating_warehouse_id=#{warehouseId} AND status='WAIT_PDA_RECEIVING' ORDER BY created_at,id")
|
||||
|
||||
+5
@@ -13,4 +13,9 @@ public interface WarehouseTransferPalletMapper extends BaseMapper<WarehouseTrans
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_pallet WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY sequence_no")
|
||||
List<WarehouseTransferPallet> listOrder(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_pallet WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} AND id=#{id} FOR UPDATE")
|
||||
WarehouseTransferPallet lockOne(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId,
|
||||
@Param("id") Long id);
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.lastleg.transfer.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/** 仓储转运单状态。状态编码持久化到数据库,中文名称用于接口和前端展示。 */
|
||||
public enum WarehouseTransferStatus {
|
||||
DRAFT("草稿"),
|
||||
PENDING_REVIEW("待审核"),
|
||||
APPROVED("审核通过"),
|
||||
REJECTED("审核驳回"),
|
||||
WAIT_PDA_RECEIVING("待PDA收货"),
|
||||
INBOUND_MEASURING("入库测量中"),
|
||||
WAIT_CUSTOMER_CONFIRMATION("待客户确认实测"),
|
||||
WAIT_RELABEL("待换标"),
|
||||
WAIT_FBA_DESTINATION("待确认目的仓"),
|
||||
READY_FOR_DISPATCH("待排车"),
|
||||
WAIT_OUTBOUND("待出库"),
|
||||
PENDING_ASSIGNMENT("待分配"),
|
||||
ALLOCATED("已分配库存"),
|
||||
OUTBOUNDED("已出库"),
|
||||
PARTIALLY_RECEIVED("部分收货"),
|
||||
RECEIVED("已收货"),
|
||||
INBOUND_COMPLETED("入库完成"),
|
||||
COMPLETED("已完成");
|
||||
|
||||
private final String label;
|
||||
|
||||
WarehouseTransferStatus(String label) { this.label = label; }
|
||||
|
||||
public String code() { return name(); }
|
||||
public String label() { return label; }
|
||||
|
||||
public static WarehouseTransferStatus fromCode(String code) {
|
||||
return Arrays.stream(values()).filter(value -> value.name().equals(code)).findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unsupported warehouse transfer status: " + code));
|
||||
}
|
||||
}
|
||||
+876
-161
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user