优化提交

This commit is contained in:
2026-09-14 09:58:30 +08:00
parent 5ffd6c0118
commit 8cf7c4b9c9
93 changed files with 2945 additions and 188 deletions
@@ -2,6 +2,7 @@ package com.baiyue.warehouse.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.warehouse.dto.DropshipPalletRequests;
import com.baiyue.warehouse.dto.RelabelPalletRequests;
import com.baiyue.warehouse.entity.WarehousePalletEntity;
import com.baiyue.warehouse.dto.WarehousePalletResolveResponse;
import com.baiyue.warehouse.security.InternalServiceTokenGuard;
@@ -24,6 +25,14 @@ public class InternalWarehousePalletController {
return ApiResponse.success(service.ensureDropshipPallet(request.customerId(), request.warehouseId(), request.businessId()));
}
@PostMapping("/ensure-relabel")
public ApiResponse<WarehousePalletResolveResponse> ensureRelabel(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody RelabelPalletRequests.Ensure request) {
tokenGuard.verify(token);
return ApiResponse.success(service.ensureRelabelPallet(request.warehouseId(), request.businessId()));
}
@GetMapping("/resolve/{palletCode}")
public ApiResponse<WarehousePalletResolveResponse> resolve(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@@ -1,7 +1,6 @@
package com.baiyue.warehouse.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
@@ -12,7 +11,7 @@ public final class PalletTransitionRequests {
public record Apply(@Positive long warehouseId,
@PositiveOrZero int expectedVersion,
@NotBlank @Size(max = 20) String targetStatus,
@NotNull @Positive Long targetLocationId,
@Positive Long targetLocationId,
@NotBlank @Size(max = 128) String operationKey,
@Size(max = 500) String reason) { }
@@ -7,8 +7,8 @@ public record PalletTransitionResponse(
long warehouseId,
String beforeStatus,
String afterStatus,
long beforeLocationId,
long afterLocationId,
Long beforeLocationId,
Long afterLocationId,
int beforeVersion,
int afterVersion,
String operationStatus,
@@ -0,0 +1,12 @@
package com.baiyue.warehouse.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
public final class RelabelPalletRequests {
private RelabelPalletRequests() { }
public record Ensure(@Positive long warehouseId,
@NotBlank @Size(max = 128) String businessId) { }
}
@@ -4,7 +4,7 @@ public record WarehousePalletResolveResponse(
long palletId,
String palletCode,
long warehouseId,
long locationId,
Long locationId,
String status,
String fbaWarehouseCode,
java.math.BigDecimal unitPrice,
@@ -14,13 +14,13 @@ public record WarehousePalletResolveResponse(
boolean reservable,
String reservationBlockReason
) {
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, long locationId,
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, Long locationId,
String status, String fbaWarehouseCode, int version) {
this(palletId, palletCode, warehouseId, locationId, status, fbaWarehouseCode,
java.math.BigDecimal.ZERO, version, null, null, false, "LOCATION_STATE_UNKNOWN");
}
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, long locationId,
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, Long locationId,
String status, String fbaWarehouseCode,
java.math.BigDecimal unitPrice, int version) {
this(palletId, palletCode, warehouseId, locationId, status, fbaWarehouseCode,
@@ -39,6 +39,6 @@ public interface WarehousePalletTransitionMapper extends BaseMapper<WarehousePal
"updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{palletId} AND customer_id=0 " +
"AND warehouse_id=#{warehouseId} AND version=#{expectedVersion} AND status<>'SCRAPPED' AND deleted=0")
int transition(@Param("warehouseId") long warehouseId, @Param("palletId") long palletId,
@Param("targetStatus") String targetStatus, @Param("targetLocationId") long targetLocationId,
@Param("targetStatus") String targetStatus, @Param("targetLocationId") Long targetLocationId,
@Param("expectedVersion") int expectedVersion);
}
@@ -45,16 +45,17 @@ public class WarehousePalletTransitionService {
if (version != request.expectedVersion()) {
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请重新扫码");
}
if (pallet.getLocationId() == null) {
throw new BizException(ResultCode.CONFLICT, "托盘没有有效库位,不能执行库内作业");
}
transition(pallet, status, request.targetLocationId(), version);
// 当前海外尾程按托盘作业,不要求业务端选择库位。未传目标库位时保留托盘现有
// 技术库位;历史无库位托盘则只流转托盘状态,不能因此阻断 PDA 作业。
Long targetLocationId = request.targetLocationId() == null
? pallet.getLocationId() : request.targetLocationId();
transition(pallet, status, targetLocationId, version);
WarehousePalletOperationEntity operation = new WarehousePalletOperationEntity();
operation.setId(IdWorker.getId()); operation.setOperationKey(key); operation.setRequestHash(requestHash);
operation.setWarehouseId(request.warehouseId()); operation.setPalletId(palletId);
operation.setPalletCode(pallet.getPalletCode()); operation.setBeforeStatus(pallet.getStatus());
operation.setAfterStatus(status); operation.setBeforeLocationId(pallet.getLocationId());
operation.setAfterLocationId(request.targetLocationId()); operation.setBeforeVersion(version);
operation.setAfterLocationId(targetLocationId); operation.setBeforeVersion(version);
operation.setAfterVersion(version + 1); operation.setOperationStatus("APPLIED");
operation.setReason(trim(request.reason())); operation.setCreatedAt(LocalDateTime.now());
operation.setUpdatedAt(operation.getCreatedAt()); mapper.insert(operation);
@@ -75,7 +76,7 @@ public class WarehousePalletTransitionService {
}
WarehousePalletEntity pallet = mapper.lockPublicPallet(request.warehouseId(), palletId);
if (pallet == null || !operation.getAfterStatus().equals(pallet.getStatus())
|| !Long.valueOf(operation.getAfterLocationId()).equals(pallet.getLocationId())) {
|| !java.util.Objects.equals(operation.getAfterLocationId(), pallet.getLocationId())) {
throw new BizException(ResultCode.CONFLICT, "托盘已被后续作业修改,自动补偿失败");
}
transition(pallet, operation.getBeforeStatus(), operation.getBeforeLocationId(),
@@ -85,14 +86,18 @@ public class WarehousePalletTransitionService {
return response(operation, false);
}
private void transition(WarehousePalletEntity pallet, String targetStatus, long targetLocationId,
private void transition(WarehousePalletEntity pallet, String targetStatus, Long targetLocationId,
int expectedVersion) {
int currentVersion = pallet.getVersion() == null ? 0 : pallet.getVersion();
if (currentVersion != expectedVersion || !MUTABLE_STATUSES.contains(pallet.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "托盘版本或状态已变化,请重新扫码");
}
long warehouseId = pallet.getWarehouseId();
long sourceLocationId = pallet.getLocationId();
Long sourceLocationId = pallet.getLocationId();
if (sourceLocationId == null || targetLocationId == null) {
transitionWithOptionalLocation(pallet, sourceLocationId, targetLocationId, targetStatus, expectedVersion);
return;
}
WarehouseLocationEntity source;
WarehouseLocationEntity target;
if (sourceLocationId <= targetLocationId) {
@@ -116,6 +121,25 @@ public class WarehousePalletTransitionService {
}
}
private void transitionWithOptionalLocation(WarehousePalletEntity pallet, Long sourceLocationId,
Long targetLocationId, String targetStatus,
int expectedVersion) {
long warehouseId = pallet.getWarehouseId();
if (sourceLocationId != null) {
WarehouseLocationEntity source = requireLocation(warehouseId, sourceLocationId);
requireLocationMatchesPallet(source, pallet.getStatus());
releaseSourceLocation(warehouseId, source, pallet.getStatus());
}
if (targetLocationId != null) {
WarehouseLocationEntity target = requireLocation(warehouseId, targetLocationId);
requireAvailableTarget(target, targetStatus);
occupyTargetLocation(warehouseId, target, targetStatus);
}
if (mapper.transition(warehouseId, pallet.getId(), targetStatus, targetLocationId, expectedVersion) != 1) {
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请重新扫码");
}
}
private WarehouseLocationEntity requireLocation(long warehouseId, long locationId) {
WarehouseLocationEntity location = mapper.lockPublicLocation(warehouseId, locationId);
if (location == null || "INACTIVE".equals(location.getStatus())) {
@@ -116,6 +116,30 @@ public class WarehouseResourceService {
}
}
@Transactional
public WarehousePalletResolveResponse ensureRelabelPallet(long warehouseId, String businessId) {
String normalizedBusinessId = businessId == null ? null : businessId.trim();
WarehousePalletBindingEntity existing = repository.findPalletBinding(PUBLIC_RESOURCE_SCOPE, warehouseId,
"RELABEL_PDA_NEW", normalizedBusinessId);
if (existing != null) return resolvePublicPallet(existing.getPalletId());
Long palletId = createPallet(PUBLIC_RESOURCE_SCOPE, warehouseId,
new PalletRequests.SaveRequest(null, "STANDARD", null, java.math.BigDecimal.ZERO, "IDLE", null,
"PDA换标拆托新托盘"));
WarehousePalletBindingEntity binding = new WarehousePalletBindingEntity();
binding.setId(IdWorker.getId()); binding.setCustomerId(PUBLIC_RESOURCE_SCOPE); binding.setWarehouseId(warehouseId);
binding.setBusinessType("RELABEL_PDA_NEW"); binding.setBusinessId(normalizedBusinessId); binding.setPalletId(palletId);
binding.setCreatedAt(java.time.LocalDateTime.now());
try {
repository.insertPalletBinding(binding);
return resolvePublicPallet(palletId);
} catch (org.springframework.dao.DuplicateKeyException duplicate) {
WarehousePalletBindingEntity winner = repository.findPalletBinding(PUBLIC_RESOURCE_SCOPE, warehouseId,
"RELABEL_PDA_NEW", normalizedBusinessId);
if (winner == null) throw duplicate;
return resolvePublicPallet(winner.getPalletId());
}
}
/**
* Returns the warehouse-scoped virtual location used only as a pallet/inventory reference.
* It is deliberately never made IDLE/OCCUPIED by resource locking.
@@ -186,7 +210,6 @@ public class WarehouseResourceService {
if (matches.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "托盘码不存在");
if (matches.size() != 1) throw new BizException(ResultCode.CONFLICT, "托盘码在多个仓库重复,无法解析");
WarehousePalletEntity pallet = matches.get(0);
if (pallet.getLocationId() == null) throw new BizException(ResultCode.CONFLICT, "托盘未绑定库位");
if ("SCRAPPED".equals(pallet.getStatus())) throw new BizException(ResultCode.CONFLICT, "托盘已报废");
return palletReference(pallet);
}
@@ -194,7 +217,6 @@ public class WarehouseResourceService {
public WarehousePalletResolveResponse resolvePublicPallet(long palletId) {
WarehousePalletEntity pallet = repository.findPublicPallet(palletId);
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在");
if (pallet.getLocationId() == null) throw new BizException(ResultCode.CONFLICT, "托盘未绑定库位");
return palletReference(pallet);
}
@@ -218,8 +240,8 @@ public class WarehouseResourceService {
}
private WarehousePalletResolveResponse palletReference(WarehousePalletEntity pallet) {
WarehouseLocationEntity location = repository.findLocation(PUBLIC_RESOURCE_SCOPE,
pallet.getWarehouseId(), pallet.getLocationId());
WarehouseLocationEntity location = pallet.getLocationId() == null ? null
: repository.findLocation(PUBLIC_RESOURCE_SCOPE, pallet.getWarehouseId(), pallet.getLocationId());
String blockReason = reservationBlockReason(pallet, location);
return new WarehousePalletResolveResponse(pallet.getId(), pallet.getPalletCode(), pallet.getWarehouseId(),
pallet.getLocationId(), pallet.getStatus(), pallet.getFbaWarehouseCode(), pallet.getUnitPrice(),
@@ -230,7 +252,7 @@ public class WarehouseResourceService {
private static String reservationBlockReason(WarehousePalletEntity pallet, WarehouseLocationEntity location) {
if (!"IDLE".equals(pallet.getStatus())) return "PALLET_NOT_IDLE";
if (location == null) return "LOCATION_NOT_FOUND";
if (location == null) return pallet.getLocationId() == null ? "LOCATION_NOT_REQUIRED" : "LOCATION_NOT_FOUND";
if ("VIRTUAL".equals(location.getLocationType())) {
return "ACTIVE".equals(location.getStatus()) ? null : "VIRTUAL_LOCATION_NOT_ACTIVE";
}