feat: support overseas warehouse locations and A0 overflow

This commit is contained in:
2026-09-22 09:38:49 +08:00
parent d3ce268ca3
commit 1ebc128c59
15 changed files with 203 additions and 15 deletions
@@ -14,6 +14,7 @@ public record InboundPlanResponse(
String customerName,
String orderType,
String containerNo,
Long overseasWarehouseId,
String overseasWarehouseCode,
String overseasWarehouseName,
String status,
@@ -1124,7 +1124,7 @@ public class InboundPlanService {
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getSourceOrderId(),
sourceOrder == null ? null : sourceOrder.orderNo(),
sourceOrder == null ? null : sourceOrder.customerName(),
sourceOrder == null ? null : sourceOrder.orderType(), plan.getContainerNo(),
sourceOrder == null ? null : sourceOrder.orderType(), plan.getContainerNo(), authoritativeWarehouse.warehouseId(),
authoritativeWarehouse.warehouseCode(), authoritativeWarehouse.warehouseName(),
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), totalGrossWeightKg,
totalNetWeightKg, plan.getTotalVolumeCbm(),
@@ -0,0 +1,42 @@
package com.baiyue.lastleg.pda.move.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.pda.move.dto.PlatformInventoryMoveRequest;
import com.baiyue.lastleg.pda.move.dto.PdaInventoryMoveResponse;
import com.baiyue.lastleg.pda.move.service.PdaInventoryMoveService;
import com.baiyue.lastleg.security.OverseasPermissions;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
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.server.ResponseStatusException;
/** Browser/platform entry point; the PDA entry point remains restricted to warehouse operators. */
@RestController
@RequestMapping("/api/platform/v1/last-leg/inventory-moves")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EDIT + "')")
public class PlatformInventoryMoveController {
@Resource private PdaInventoryMoveService service;
@PostMapping
public ApiResponse<PdaInventoryMoveResponse> move(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody PlatformInventoryMoveRequest request) {
requirePlatform(user);
String operator = user.userId() == null ? user.username() : String.valueOf(user.userId());
return ApiResponse.success(service.movePlatform(request.overseasWarehouseId(), operator,
request.asPdaRequest()));
}
private static void requirePlatform(AuthenticatedUser user) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform identity is required");
}
}
}
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.pda.move.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
/** Platform/admin variant of the pallet put-away command. */
public record PlatformInventoryMoveRequest(
@Positive long overseasWarehouseId,
@NotBlank @Size(max = 128) String sourcePalletCode,
@NotBlank @Size(max = 128) String targetLocationCode,
@PositiveOrZero int expectedPalletVersion,
@NotBlank @Size(max = 128) String idempotencyKey,
@NotBlank @Size(max = 500) String reason
) {
public PdaInventoryMoveRequest asPdaRequest() {
return new PdaInventoryMoveRequest(sourcePalletCode, targetLocationCode,
expectedPalletVersion, idempotencyKey, reason);
}
}
@@ -45,6 +45,22 @@ public class PdaInventoryMoveService {
@Transactional
public PdaInventoryMoveResponse move(long warehouseId, String operatorKey, PdaInventoryMoveRequest request) {
return moveInternal(warehouseId, operatorKey, request);
}
/**
* Platform/admin put-away uses the same command, idempotency record and physical
* transition as the PDA flow. Keeping one implementation prevents the two entry
* points from drifting on A0, virtual-location and inventory-position semantics.
*/
@Transactional
public PdaInventoryMoveResponse movePlatform(long warehouseId, String operatorKey,
PdaInventoryMoveRequest request) {
return moveInternal(warehouseId, "PLATFORM:" + required(operatorKey, "Operator is required"), request);
}
private PdaInventoryMoveResponse moveInternal(long warehouseId, String operatorKey,
PdaInventoryMoveRequest request) {
String key = required(request.idempotencyKey(), "Idempotency key is required");
String requestHash = hash(request);
PdaInventoryMove existing = moves.find(warehouseId, operatorKey, key);
@@ -58,6 +74,11 @@ public class PdaInventoryMoveService {
if (!Set.of("ACTIVE", "IDLE", "OCCUPIED").contains(target.status())) {
throw conflict("Target location is not available");
}
// A virtual receiving location is an intake staging position, not a put-away
// destination. A0 is deliberately allowed as a shared overflow location.
if ("VIRTUAL".equalsIgnoreCase(target.locationType())) {
throw conflict("Virtual receiving location cannot be used as a put-away target");
}
boolean recoveringRemoteSuccess = pallet.locationId() == target.locationId()
&& pallet.version() == request.expectedPalletVersion() + 1;
if (!recoveringRemoteSuccess && pallet.version() != request.expectedPalletVersion()) {