This commit is contained in:
2026-09-10 17:11:33 +08:00
parent 1ee32b7a6f
commit 2e6a18c58e
54 changed files with 2094 additions and 167 deletions
@@ -37,7 +37,7 @@ public class DeliveryTransferController {
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_CREATE + "')")
@Operation(summary = "Create one-click dropship outbound and push a PDA picking task")
public ApiResponse<DeliveryResponses.PdaTask> createOneClick(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
@Valid @RequestBody DeliveryRequests.OneClickOutbound request) {
return ApiResponse.success(service.createOneClick(requiredCustomer(user, customerId), operator(user), request));
}
@@ -80,7 +80,12 @@ public final class DeliveryRequests {
}
}
public record PdaPick(@NotNull Long outboundPalletItemId, Long inventoryPositionId,
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey) {}
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey,
@Size(max=500) List<@NotBlank @Size(max=255) String> cartonMarks) {
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey) {
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, List.of());
}
}
public record PdaComplete(@NotBlank @Size(max=255) String idempotencyKey) {}
public record Cancel(@NotBlank @Size(max=1000) String reason) {}
}
@@ -50,6 +50,12 @@ import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
@@ -88,6 +94,9 @@ public class DeliveryTransferService {
@Resource private LastlegOrderMapper tailOrders;
@Resource private LastlegOrderStatusHistoryMapper histories;
@Resource private LastlegOrderDetailMapper tailOrderDetails;
@Resource private RelabelApplicationMapper relabelApplications;
@Resource private RelabelItemMapper relabelItems;
@Resource private RelabelApplicationService relabelService;
@Resource private LastlegOrderNoGenerator orderNoGenerator;
@Transactional
@@ -358,7 +367,7 @@ public class DeliveryTransferService {
if (replay != null) {
if (!task.getId().equals(replay.getTaskId())
|| !request.outboundPalletItemId().equals(replay.getOutboundPalletItemId())
|| !request.inventoryPositionId().equals(replay.getInventoryPositionId())
|| !Objects.equals(request.inventoryPositionId(), replay.getInventoryPositionId())
|| !request.packages().equals(replay.getPackages())) {
throw conflict("Idempotency key was reused with a different outbound pick request");
}
@@ -393,6 +402,62 @@ public class DeliveryTransferService {
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
return taskResult(outbound, task);
}
if ("RELABEL_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("确认的库存位置不属于该换标出库托盘");
}
List<String> expectedMarks = Arrays.stream(Optional.ofNullable(item.getCartonMarks()).orElse("").split(","))
.map(String::trim).filter(StringUtils::hasText).distinct().sorted().toList();
List<String> submittedMarks = request.cartonMarks() == null ? List.of() : request.cartonMarks();
List<String> scannedMarks = submittedMarks.stream().map(String::trim).filter(StringUtils::hasText)
.distinct().sorted().toList();
if (scannedMarks.size() != submittedMarks.size() || !expectedMarks.equals(scannedMarks)) {
throw conflict("必须扫描并提交该换标出库任务的全部指定箱唛");
}
if (request.packages().intValue() != value(item.getPackageCount()) || value(item.getPickedPackageCount()) > 0) {
throw conflict("换标出库托盘必须按指定箱唛一次完成拆托确认");
}
RelabelApplication application = relabelApplications.lockByOutbound(customerId, outbound.getOutboundNo());
if (application == null || !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
throw conflict("换标单尚未完成客户确认或已不可出库");
}
List<RelabelItem> sourceItems = relabelItems.selectList(new LambdaQueryWrapper<RelabelItem>()
.eq(RelabelItem::getApplicationId, application.getId())
.eq(RelabelItem::getCustomerId, customerId)
.eq(RelabelItem::getSourceInventoryPositionId, request.inventoryPositionId()));
long sourcePackages = sourceItems.stream().mapToLong(source -> value(source.getNotifiedPackageCount())).sum();
if (sourcePackages != request.packages()) throw conflict("换标出库来源库存快照与任务箱数不一致");
int releaseIndex = 0;
for (RelabelItem source : sourceItems) {
inventoryCommands.release(customerId, operatorId, new InventoryCommandRequests.Move(
request.inventoryPositionId(), value(source.getNotifiedPackageCount()), application.getRelabelNo(), null,
"RELABEL:" + application.getRelabelNo(), request.idempotencyKey() + ":RELEASE:" + releaseIndex++,
"换标出库拆托前释放原冻结库存"));
}
inventoryCommands.allocate(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(),
request.packages(), application.getRelabelNo(), outbound.getOutboundNo(), null,
request.idempotencyKey() + ":ALLOCATE", "换标出库指定箱唛拆托"));
inventoryCommands.pick(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(),
request.packages(), application.getRelabelNo(), outbound.getOutboundNo(), null,
request.idempotencyKey() + ":PICK", "换标出库指定箱唛拣货"));
inventoryCommands.ship(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(),
request.packages(), application.getRelabelNo(), 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(request.inventoryPositionId());
pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
List<OutboundPalletItem> pickedItems = outboundPallets.findByOutbound(customerId, outbound.getId());
if (!pickedItems.isEmpty() && pickedItems.stream().allMatch(value -> "PICKED".equals(value.getStatus()))) {
outbound.setStatus("PICKED"); outbound.setUpdatedAt(LocalDateTime.now()); outboundOrders.updateById(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())) {
@@ -452,6 +517,13 @@ public class DeliveryTransferService {
task.setStatus("COMPLETED"); task.setCompletionIdempotencyKey(completionKey);
task.setCompletedAt(now); task.setUpdatedAt(now); pdaTasks.updateById(task);
outbound.setStatus("OUTBOUNDED"); outbound.setUpdatedAt(now); outboundOrders.updateById(outbound);
if ("RELABEL_OUTBOUND".equals(outbound.getOutboundType())) {
RelabelApplication application = relabelApplications.lockByOutbound(customerId, outbound.getOutboundNo());
if (application != null && RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
relabelService.changeStatus(customerId, application.getRelabelNo(), "PDA_OUTBOUND",
new RelabelRequests.ChangeStatus("COMPLETED", "换标出库PDA作业完成"));
}
}
return taskResult(outbound, task);
}