优化提交

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
@@ -28,6 +28,13 @@ public class PdaPalletOperationController {
return ApiResponse.success(service.resolve(PdaWarehouseIdentity.require(user), request));
}
@PostMapping("/relabel-pallet-label")
public ApiResponse<PalletOperationResponses.PrintablePallet> createRelabelPallet(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody PalletOperationRequests.NewRelabelPallet request) {
return ApiResponse.success(service.createRelabelPallet(PdaWarehouseIdentity.require(user), request));
}
@PostMapping("/assemble")
public ApiResponse<PalletOperationResponses.Operation> assemble(
@AuthenticationPrincipal AuthenticatedUser user,
@@ -13,6 +13,8 @@ public final class PalletOperationRequests {
private PalletOperationRequests() { }
public record Scan(@NotBlank @Size(max = 128) String code) { }
public record NewRelabelPallet(@NotBlank @Size(max = 64) String relabelNo,
@NotBlank @Size(max = 128) String idempotencyKey) { }
public record PalletExpectation(@NotBlank @Size(max = 128) String palletCode,
@PositiveOrZero int expectedVersion) { }
public record MoveLine(@Positive long positionId, @Positive long packages,
@@ -11,8 +11,10 @@ public final class PalletOperationResponses {
long availablePackages, long holdPackages, long allocatedPackages,
long pickedPackages, long receivingPackages, long positionVersion) { }
public record PalletContent(long palletId, String palletCode, String status, int palletVersion,
long locationId, String locationCode, String destinationWarehouseCode,
Long locationId, String locationCode, String destinationWarehouseCode,
long totalPackages, List<InventoryLine> inventory) { }
public record PrintablePallet(long palletId, String palletCode, int palletVersion,
String relabelNo, String printableHtml) { }
public record Operation(long operationId, String operationType, String idempotencyKey,
String status, boolean replayed, LocalDateTime completedAt,
List<PalletContent> pallets) {
@@ -28,6 +28,21 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway {
restClientProperties.getPalletPhysical());
}
@Override
public PalletReference ensureRelabelPallet(long warehouseId, String businessId) {
Map<String, Object> body = Map.of("warehouseId", warehouseId, "businessId", businessId);
ResolveEnvelope response = client.post().uri("/internal/v1/warehouse-pallets/ensure-relabel")
.header("X-Internal-Service-Token", internalToken).body(body).retrieve()
.onStatus(HttpStatusCode::isError, (request, result) -> {
throw palletLookupFailure(result.getStatusCode());
}).body(ResolveEnvelope.class);
if (response == null || response.data() == null) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
"Warehouse pallet service returned no usable response");
}
return response.data();
}
@Override
public PalletReference resolve(String palletCode) {
ResolveEnvelope response = client.get().uri("/internal/v1/warehouse-pallets/resolve/{code}", palletCode)
@@ -76,11 +91,12 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway {
}
@Override
public Transition apply(long warehouseId, PalletReference pallet, String targetStatus, long targetLocationId,
public Transition apply(long warehouseId, PalletReference pallet, String targetStatus, Long targetLocationId,
String operationKey, String reason) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("warehouseId", warehouseId); body.put("expectedVersion", pallet.version());
body.put("targetStatus", targetStatus); body.put("targetLocationId", targetLocationId);
body.put("targetStatus", targetStatus);
if (targetLocationId != null) body.put("targetLocationId", targetLocationId);
body.put("operationKey", operationKey); if (reason != null) body.put("reason", reason);
TransitionEnvelope response = client.post().uri("/internal/v1/pda-pallet-transitions/{id}", pallet.palletId())
.header("X-Internal-Service-Token", internalToken).body(body).retrieve()
@@ -1,16 +1,17 @@
package com.baiyue.lastleg.palletops.gateway;
public interface PalletPhysicalGateway {
PalletReference ensureRelabelPallet(long warehouseId, String businessId);
PalletReference resolve(String palletCode);
PalletReference resolve(long palletId);
LocationReference resolveLocation(long warehouseId, long locationId);
Transition apply(long warehouseId, PalletReference pallet, String targetStatus, long targetLocationId,
Transition apply(long warehouseId, PalletReference pallet, String targetStatus, Long targetLocationId,
String operationKey, String reason);
void compensate(long warehouseId, Transition transition);
record PalletReference(long palletId, String palletCode, long warehouseId, long locationId,
record PalletReference(long palletId, String palletCode, long warehouseId, Long locationId,
String status, String fbaWarehouseCode, java.math.BigDecimal unitPrice, int version) {
public PalletReference(long palletId, String palletCode, long warehouseId, long locationId,
public PalletReference(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);
@@ -18,6 +19,6 @@ public interface PalletPhysicalGateway {
}
record LocationReference(long locationId, String locationCode, String locationType, String status) { }
record Transition(String operationKey, long palletId, String palletCode, long warehouseId,
String beforeStatus, String afterStatus, long beforeLocationId, long afterLocationId,
String beforeStatus, String afterStatus, Long beforeLocationId, Long afterLocationId,
int beforeVersion, int afterVersion, String operationStatus, boolean replayed) { }
}
@@ -30,6 +30,7 @@ import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
@@ -77,6 +78,30 @@ public class PdaPalletOperationService {
return content(identity.overseasWarehouseId(), pallet);
}
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("换标单尚未完成客户确认,不能生成新托盘");
}
if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
throw conflict("换标单不属于 Token 冻结的海外仓");
}
String key = required(request.idempotencyKey(), "幂等键不能为空");
PalletPhysicalGateway.PalletReference pallet = physical.ensureRelabelPallet(identity.overseasWarehouseId(),
relabelNo + ":" + key);
String code = HtmlUtils.htmlEscape(pallet.palletCode());
String order = HtmlUtils.htmlEscape(relabelNo);
String html = "<!doctype html><html><head><meta charset=\"UTF-8\"><title>" + code
+ "</title><style>@page{size:100mm 70mm;margin:4mm}body{font-family:Arial,sans-serif;text-align:center;margin:0}"
+ "h1{font-size:24px;margin:8mm 0 4mm;letter-spacing:0}.code{font-size:18px;font-weight:700;word-break:break-all}"
+ ".ref{font-size:12px;margin-top:6mm}</style></head><body><h1>托盘标签</h1><div class=\"code\">"
+ code + "</div><div class=\"ref\">换标单:" + order + "</div></body></html>";
return new PalletOperationResponses.PrintablePallet(pallet.palletId(), pallet.palletCode(), pallet.version(),
relabelNo, html);
}
public PalletOperationResponses.Operation assemble(PdaWarehouseIdentity identity,
PalletOperationRequests.Assemble request) {
PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_ASSEMBLE",
@@ -175,8 +200,10 @@ public class PdaPalletOperationService {
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));
String reason = "换标拆托" + (request.reason() == null || request.reason().isBlank()
? "" : ":" + request.reason().trim());
return execute(identity, "RELABEL_PALLET_DISASSEMBLE", request.idempotencyKey(), request,
request.reason(), relabelPlan);
reason, relabelPlan);
}
public PalletOperationResponses.Operation swap(PdaWarehouseIdentity identity,
@@ -563,9 +590,10 @@ public class PdaPalletOperationService {
.digest((operator + "|" + key + "|" + palletId).getBytes(StandardCharsets.UTF_8))); }
catch (NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
}
private String locationCode(long warehouseId, long locationId) {
private String locationCode(long warehouseId, Long locationId) {
if (locationId == null) return null;
PalletPhysicalGateway.LocationReference location = physical.resolveLocation(warehouseId, locationId);
if (location == null || location.locationId() != locationId || "INACTIVE".equals(location.status())) {
if (location == null || location.locationId() != locationId.longValue() || "INACTIVE".equals(location.status())) {
throw conflict("托盘库位不存在或已停用");
}
return location.locationCode();
@@ -622,10 +650,10 @@ public class PdaPalletOperationService {
private record PlannedMove(long positionId, long packages, long expectedVersion,
long sourcePalletId, String sourcePalletCode,
long targetPalletId, String targetPalletCode, long targetLocationId,
long targetPalletId, String targetPalletCode, Long targetLocationId,
String targetLocationCode, InventoryBucket bucket) { }
private record PalletChange(PalletPhysicalGateway.PalletReference pallet, String targetStatus,
long targetLocationId) { }
Long targetLocationId) { }
private record Plan(Map<String, PalletPhysicalGateway.PalletReference> pallets,
List<PlannedMove> moves, List<PalletChange> changes,
RelabelMutation relabelMutation) { }