feat: complete overseas pda warehouse operations
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
package com.baiyue.lastleg.palletops.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.palletops.dto.PalletOperationRequests;
|
||||
import com.baiyue.lastleg.palletops.dto.PalletOperationResponses;
|
||||
import com.baiyue.lastleg.palletops.service.PdaPalletOperationService;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/pallet-operations")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')")
|
||||
public class PdaPalletOperationController {
|
||||
@Resource private PdaPalletOperationService service;
|
||||
|
||||
@PostMapping("/pallets/resolve")
|
||||
public ApiResponse<PalletOperationResponses.PalletContent> resolve(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.Scan request) {
|
||||
return ApiResponse.success(service.resolve(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/assemble")
|
||||
public ApiResponse<PalletOperationResponses.Operation> assemble(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.Assemble request) {
|
||||
return ApiResponse.success(service.assemble(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/disassemble")
|
||||
public ApiResponse<PalletOperationResponses.Operation> disassemble(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.Disassemble request) {
|
||||
return ApiResponse.success(service.disassemble(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/swap")
|
||||
public ApiResponse<PalletOperationResponses.Operation> swap(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.Swap request) {
|
||||
return ApiResponse.success(service.swap(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/unbind")
|
||||
public ApiResponse<PalletOperationResponses.Operation> unbind(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.Unbind request) {
|
||||
return ApiResponse.success(service.unbind(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.lastleg.palletops.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class PalletOperationRequests {
|
||||
private PalletOperationRequests() { }
|
||||
|
||||
public record Scan(@NotBlank @Size(max = 128) String code) { }
|
||||
public record PalletExpectation(@NotBlank @Size(max = 128) String palletCode,
|
||||
@PositiveOrZero int expectedVersion) { }
|
||||
public record MoveLine(@Positive long positionId, @Positive long packages,
|
||||
@PositiveOrZero long expectedPositionVersion) { }
|
||||
public record TargetMove(@Valid PalletExpectation target,
|
||||
@NotEmpty @Size(max = 200) List<@Valid MoveLine> lines) { }
|
||||
|
||||
public record Assemble(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@Valid PalletExpectation target,
|
||||
@NotEmpty @Size(max = 20) List<@Valid PalletExpectation> sources,
|
||||
@NotEmpty @Size(max = 200) List<@Valid MoveLine> lines,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
public record Disassemble(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@Valid PalletExpectation source,
|
||||
@NotEmpty @Size(max = 20) List<@Valid TargetMove> targets,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
public record Swap(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@Valid PalletExpectation source,
|
||||
@Valid PalletExpectation target,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
public record Unbind(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@Valid PalletExpectation source,
|
||||
@Size(max = 500) String reason) { }
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.palletops.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class PalletOperationResponses {
|
||||
private PalletOperationResponses() { }
|
||||
|
||||
public record InventoryLine(String customerId, String customerName, long balanceId, long positionId,
|
||||
String inboundCode, String skuCode, String productName,
|
||||
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 totalPackages, List<InventoryLine> inventory) { }
|
||||
public record Operation(long operationId, String operationType, String idempotencyKey,
|
||||
String status, boolean replayed, LocalDateTime completedAt,
|
||||
List<PalletContent> pallets) {
|
||||
public Operation asReplay() {
|
||||
return new Operation(operationId, operationType, idempotencyKey, status, true, completedAt, pallets);
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.palletops.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_pda_pallet_operation")
|
||||
public class PdaPalletOperation {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private Long overseasWarehouseId;
|
||||
private String operatorKey;
|
||||
private String idempotencyKey;
|
||||
private String operationType;
|
||||
private String requestHash;
|
||||
private String status;
|
||||
private String resultJson;
|
||||
private String remoteTransitionsJson;
|
||||
private String lastError;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.baiyue.lastleg.palletops.gateway;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class HttpPalletPhysicalGateway implements PalletPhysicalGateway {
|
||||
@Resource private RestClient.Builder restClientBuilder;
|
||||
@Value("${baiyue.warehouse.base-url:http://127.0.0.1:9305}") private String baseUrl;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
private RestClient client;
|
||||
|
||||
@PostConstruct void initialize() { client = restClientBuilder.baseUrl(baseUrl).build(); }
|
||||
|
||||
@Override
|
||||
public PalletReference resolve(String palletCode) {
|
||||
ResolveEnvelope response = client.get().uri("/internal/v1/warehouse-pallets/resolve/{code}", palletCode)
|
||||
.header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw new IllegalArgumentException("Warehouse rejected pallet scan: HTTP " + result.getStatusCode());
|
||||
}).body(ResolveEnvelope.class);
|
||||
if (response == null || response.data() == null) throw new IllegalStateException("Warehouse returned no pallet");
|
||||
return response.data();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocationReference resolveLocation(long warehouseId, long locationId) {
|
||||
LocationEnvelope response = client.get().uri(uri -> uri
|
||||
.path("/internal/v1/warehouse-resource-locks/locations/{locationId}")
|
||||
.queryParam("warehouseId", warehouseId).build(locationId))
|
||||
.header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw new IllegalArgumentException("Warehouse rejected location lookup: HTTP " + result.getStatusCode());
|
||||
}).body(LocationEnvelope.class);
|
||||
if (response == null || response.data() == null) throw new IllegalStateException("Warehouse returned no location");
|
||||
return response.data();
|
||||
}
|
||||
|
||||
@Override
|
||||
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("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()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw new IllegalStateException("Warehouse rejected pallet transition: HTTP " + result.getStatusCode());
|
||||
}).body(TransitionEnvelope.class);
|
||||
if (response == null || response.data() == null) throw new IllegalStateException("Warehouse returned no transition");
|
||||
return response.data();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void compensate(long warehouseId, Transition transition) {
|
||||
client.post().uri("/internal/v1/pda-pallet-transitions/{id}/{key}/compensate",
|
||||
transition.palletId(), transition.operationKey())
|
||||
.header("X-Internal-Service-Token", internalToken).body(Map.of("warehouseId", warehouseId))
|
||||
.retrieve().onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw new IllegalStateException("Warehouse rejected pallet compensation: HTTP " + result.getStatusCode());
|
||||
}).toBodilessEntity();
|
||||
}
|
||||
|
||||
private record ResolveEnvelope(PalletReference data) { }
|
||||
private record LocationEnvelope(LocationReference data) { }
|
||||
private record TransitionEnvelope(Transition data) { }
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.palletops.gateway;
|
||||
|
||||
public interface PalletPhysicalGateway {
|
||||
PalletReference resolve(String palletCode);
|
||||
LocationReference resolveLocation(long warehouseId, long locationId);
|
||||
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,
|
||||
String status, String fbaWarehouseCode, int version) { }
|
||||
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,
|
||||
int beforeVersion, int afterVersion, String operationStatus, boolean replayed) { }
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.baiyue.lastleg.palletops.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.palletops.entity.PdaPalletOperation;
|
||||
import com.baiyue.lastleg.palletops.model.PalletInventoryRow;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface PdaPalletOperationMapper extends BaseMapper<PdaPalletOperation> {
|
||||
String CONTENT_SELECT = "SELECT b.customer_id,b.customer_name,b.id balance_id,p.id position_id," +
|
||||
"b.inbound_code,b.sku_code,b.product_name_zh,b.product_name_en,b.destination_warehouse_code," +
|
||||
"p.pallet_id,p.pallet_no,p.batch_no,p.warehouse_location_id,p.warehouse_location_code," +
|
||||
"p.available_packages,p.hold_packages,p.allocated_packages,p.picked_packages,p.receiving_packages,p.version " +
|
||||
"FROM ll_inventory_position p JOIN ll_inventory_balance b ON b.id=p.balance_id AND b.customer_id=p.customer_id ";
|
||||
|
||||
@Select("SELECT * FROM ll_pda_pallet_operation WHERE operator_key=#{operatorKey} " +
|
||||
"AND idempotency_key=#{idempotencyKey} LIMIT 1")
|
||||
PdaPalletOperation findOperation(@Param("operatorKey") String operatorKey,
|
||||
@Param("idempotencyKey") String idempotencyKey);
|
||||
|
||||
@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")
|
||||
List<PalletInventoryRow> content(@Param("warehouseId") String warehouseId,
|
||||
@Param("palletId") long palletId);
|
||||
|
||||
@Select(CONTENT_SELECT + "WHERE b.overseas_warehouse_id=#{warehouseId} AND p.id=#{positionId} " +
|
||||
"LIMIT 1 FOR UPDATE")
|
||||
PalletInventoryRow lockPosition(@Param("warehouseId") String warehouseId,
|
||||
@Param("positionId") long positionId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId} " +
|
||||
"AND pallet_id=#{palletId} AND batch_no=#{batchNo} LIMIT 1 FOR UPDATE")
|
||||
InventoryPosition lockTarget(@Param("customerId") String customerId, @Param("balanceId") long balanceId,
|
||||
@Param("palletId") long palletId, @Param("batchNo") String batchNo);
|
||||
|
||||
@Select("SELECT COALESCE(SUM(available_packages+hold_packages+allocated_packages+picked_packages+receiving_packages),0) " +
|
||||
"FROM ll_inventory_position WHERE pallet_id=#{palletId} AND overseas_warehouse_id=#{warehouseId}")
|
||||
long palletPackages(@Param("warehouseId") String warehouseId, @Param("palletId") long palletId);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.lastleg.palletops.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PalletInventoryRow {
|
||||
private String customerId;
|
||||
private String customerName;
|
||||
private Long balanceId;
|
||||
private Long positionId;
|
||||
private String inboundCode;
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private String productNameEn;
|
||||
private String destinationWarehouseCode;
|
||||
private Long palletId;
|
||||
private String palletNo;
|
||||
private String batchNo;
|
||||
private Long warehouseLocationId;
|
||||
private String warehouseLocationCode;
|
||||
private Long availablePackages;
|
||||
private Long holdPackages;
|
||||
private Long allocatedPackages;
|
||||
private Long pickedPackages;
|
||||
private Long receivingPackages;
|
||||
private Long version;
|
||||
}
|
||||
+506
@@ -0,0 +1,506 @@
|
||||
package com.baiyue.lastleg.palletops.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryLedger;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryLedgerMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.palletops.dto.PalletOperationRequests;
|
||||
import com.baiyue.lastleg.palletops.dto.PalletOperationResponses;
|
||||
import com.baiyue.lastleg.palletops.entity.PdaPalletOperation;
|
||||
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
|
||||
import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper;
|
||||
import com.baiyue.lastleg.palletops.model.PalletInventoryRow;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class PdaPalletOperationService {
|
||||
@Resource private PdaPalletOperationMapper mapper;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private InventoryBalanceMapper balances;
|
||||
@Resource private InventoryLedgerMapper ledgers;
|
||||
@Resource private PalletPhysicalGateway physical;
|
||||
@Resource private PdaWarehouseService history;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
private TransactionTemplate transaction;
|
||||
private TransactionTemplate newTransaction;
|
||||
|
||||
@Resource
|
||||
void configureTransactions(PlatformTransactionManager manager) {
|
||||
transaction = new TransactionTemplate(manager);
|
||||
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
|
||||
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
newTransaction = new TransactionTemplate(manager, definition);
|
||||
}
|
||||
|
||||
public PalletOperationResponses.PalletContent resolve(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Scan request) {
|
||||
PalletPhysicalGateway.PalletReference pallet = requirePallet(identity.overseasWarehouseId(), request.code());
|
||||
return content(identity.overseasWarehouseId(), pallet);
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation assemble(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Assemble request) {
|
||||
Map<String, PalletPhysicalGateway.PalletReference> pallets = resolvePallets(identity.overseasWarehouseId(),
|
||||
request.target(), request.sources());
|
||||
PalletPhysicalGateway.PalletReference target = pallets.get(code(request.target().palletCode()));
|
||||
Map<String, PalletPhysicalGateway.PalletReference> sources = new LinkedHashMap<>();
|
||||
request.sources().forEach(value -> sources.put(code(value.palletCode()), pallets.get(code(value.palletCode()))));
|
||||
List<PlannedMove> moves = plannedLines(identity.overseasWarehouseId(), request.lines(), sources,
|
||||
target, null);
|
||||
requireCommonDestination(identity.overseasWarehouseId(), moves, List.of(target));
|
||||
return execute(identity, "PALLET_ASSEMBLE", request.idempotencyKey(), request, request.reason(),
|
||||
plan(identity.overseasWarehouseId(), pallets, moves, Set.of(target.palletId()), false));
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation disassemble(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Disassemble request) {
|
||||
LinkedHashMap<String, PalletOperationRequests.PalletExpectation> expectations = new LinkedHashMap<>();
|
||||
expectations.put(code(request.source().palletCode()), request.source());
|
||||
request.targets().forEach(value -> expectations.put(code(value.target().palletCode()), value.target()));
|
||||
Map<String, PalletPhysicalGateway.PalletReference> pallets = resolvePallets(identity.overseasWarehouseId(),
|
||||
expectations.values());
|
||||
PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode()));
|
||||
List<PlannedMove> moves = new ArrayList<>();
|
||||
for (PalletOperationRequests.TargetMove targetMove : request.targets()) {
|
||||
PalletPhysicalGateway.PalletReference target = pallets.get(code(targetMove.target().palletCode()));
|
||||
moves.addAll(plannedLines(identity.overseasWarehouseId(), targetMove.lines(), Map.of(
|
||||
code(request.source().palletCode()), source), target, source));
|
||||
}
|
||||
requireUniqueLines(moves);
|
||||
List<PalletPhysicalGateway.PalletReference> targets = request.targets().stream()
|
||||
.map(value -> pallets.get(code(value.target().palletCode()))).toList();
|
||||
requireCommonDestination(identity.overseasWarehouseId(), moves, targets);
|
||||
return execute(identity, "PALLET_DISASSEMBLE", request.idempotencyKey(), request, request.reason(),
|
||||
plan(identity.overseasWarehouseId(), pallets, moves,
|
||||
targets.stream().map(PalletPhysicalGateway.PalletReference::palletId).collect(java.util.stream.Collectors.toSet()),
|
||||
false));
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation swap(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Swap request) {
|
||||
Map<String, PalletPhysicalGateway.PalletReference> pallets = resolvePallets(identity.overseasWarehouseId(),
|
||||
request.source(), List.of(request.target()));
|
||||
PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode()));
|
||||
PalletPhysicalGateway.PalletReference target = pallets.get(code(request.target().palletCode()));
|
||||
if (!mapper.content(String.valueOf(identity.overseasWarehouseId()), target.palletId()).isEmpty()) {
|
||||
throw conflict("换托目标托盘必须为空");
|
||||
}
|
||||
List<PalletInventoryRow> rows = nonEmptyContent(identity.overseasWarehouseId(), source.palletId());
|
||||
List<PlannedMove> moves = rows.stream().map(row -> {
|
||||
requireMovable(row, value(row.getAvailablePackages()));
|
||||
return new PlannedMove(row.getPositionId(), value(row.getAvailablePackages()), value(row.getVersion()),
|
||||
source.palletId(), source.palletCode(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), target.locationId()));
|
||||
}).toList();
|
||||
return execute(identity, "PALLET_SWAP", request.idempotencyKey(), request, request.reason(),
|
||||
plan(identity.overseasWarehouseId(), pallets, moves, Set.of(target.palletId()), false));
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation unbind(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Unbind request) {
|
||||
Map<String, PalletPhysicalGateway.PalletReference> pallets = resolvePallets(identity.overseasWarehouseId(),
|
||||
List.of(request.source()));
|
||||
PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode()));
|
||||
List<PalletInventoryRow> rows = nonEmptyContent(identity.overseasWarehouseId(), source.palletId());
|
||||
List<PlannedMove> moves = rows.stream().map(row -> {
|
||||
requireMovable(row, value(row.getAvailablePackages()));
|
||||
return new PlannedMove(row.getPositionId(), value(row.getAvailablePackages()), value(row.getVersion()),
|
||||
source.palletId(), source.palletCode(), 0L, "UNBOUND", source.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), source.locationId()));
|
||||
}).toList();
|
||||
return execute(identity, "PALLET_UNBIND", request.idempotencyKey(), request, request.reason(),
|
||||
plan(identity.overseasWarehouseId(), pallets, moves, Set.of(), true));
|
||||
}
|
||||
|
||||
private PalletOperationResponses.Operation execute(PdaWarehouseIdentity identity, String type, String key,
|
||||
Object request, String reason, Plan plan) {
|
||||
String normalizedKey = required(key, "幂等键不能为空");
|
||||
String requestHash = hash(request);
|
||||
PdaPalletOperation operation = begin(identity, type, normalizedKey, requestHash);
|
||||
if ("COMPLETED".equals(operation.getStatus())) return read(operation).asReplay();
|
||||
|
||||
List<PalletPhysicalGateway.Transition> transitions = new ArrayList<>();
|
||||
try {
|
||||
for (PalletChange change : plan.changes()) {
|
||||
transitions.add(physical.apply(identity.overseasWarehouseId(), change.pallet(), change.targetStatus(),
|
||||
change.targetLocationId(), transitionKey(identity.operatorKey(), normalizedKey,
|
||||
change.pallet().palletId()), reason));
|
||||
}
|
||||
saveTransitions(operation.getId(), transitions);
|
||||
PalletOperationResponses.Operation result = transaction.execute(status ->
|
||||
mutate(identity, operation, plan, normalizedKey, requestHash, reason));
|
||||
if (result == null) throw new IllegalStateException("托盘作业事务没有返回结果");
|
||||
return result;
|
||||
} catch (RuntimeException failure) {
|
||||
boolean compensated = compensate(identity.overseasWarehouseId(), transitions);
|
||||
fail(operation.getId(), failure, compensated);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private PalletOperationResponses.Operation mutate(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
Plan plan, String key, String requestHash, String reason) {
|
||||
Set<Long> seen = new HashSet<>();
|
||||
LinkedHashSet<String> customers = new LinkedHashSet<>();
|
||||
for (PlannedMove move : plan.moves()) {
|
||||
if (!seen.add(move.positionId())) throw conflict("同一库存行不能在一次托盘作业中重复移动");
|
||||
PalletInventoryRow locked = mapper.lockPosition(String.valueOf(identity.overseasWarehouseId()),
|
||||
move.positionId());
|
||||
if (locked == null || !Objects.equals(locked.getPalletId(), move.sourcePalletId())) {
|
||||
throw conflict("库存所在托盘已变化,请重新扫码");
|
||||
}
|
||||
if (value(locked.getVersion()) != move.expectedVersion()) throw conflict("库存版本已变化,请重新扫码");
|
||||
requireMovable(locked, move.packages());
|
||||
customers.add(locked.getCustomerId());
|
||||
moveInventory(identity, operation, locked, move, key, reason);
|
||||
}
|
||||
for (PalletChange change : plan.changes()) {
|
||||
long actual = mapper.palletPackages(String.valueOf(identity.overseasWarehouseId()), change.pallet().palletId());
|
||||
boolean shouldBeEmpty = "IDLE".equals(change.targetStatus());
|
||||
if (shouldBeEmpty != (actual == 0)) {
|
||||
throw conflict("库存与实物托盘状态不一致,作业已回滚");
|
||||
}
|
||||
}
|
||||
|
||||
List<PalletOperationResponses.PalletContent> contents = plan.pallets().values().stream()
|
||||
.sorted(Comparator.comparingLong(PalletPhysicalGateway.PalletReference::palletId))
|
||||
.map(pallet -> content(identity.overseasWarehouseId(), physical.resolve(pallet.palletCode())))
|
||||
.toList();
|
||||
PalletOperationResponses.Operation result = new PalletOperationResponses.Operation(operation.getId(),
|
||||
operation.getOperationType(), key, "COMPLETED", false, LocalDateTime.now(), contents);
|
||||
operation.setStatus("COMPLETED"); operation.setResultJson(write(result)); operation.setLastError(null);
|
||||
operation.setUpdatedAt(result.completedAt()); mapper.updateById(operation);
|
||||
history.record(identity.overseasWarehouseId(), customers.size() == 1 ? customers.iterator().next() : null,
|
||||
identity.operatorKey(), operation.getOperationType(), businessKey(plan), "COMPLETED", key, requestHash,
|
||||
"托盘作业完成,移动库存行 " + plan.moves().size() + " 条");
|
||||
return result;
|
||||
}
|
||||
|
||||
private void moveInventory(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
PalletInventoryRow sourceRow, PlannedMove move, String key, String reason) {
|
||||
InventoryPosition source = positions.selectById(sourceRow.getPositionId());
|
||||
if (source == null) throw conflict("库存行不存在");
|
||||
long sourceBefore = value(source.getAvailablePackages());
|
||||
source.setAvailablePackages(sourceBefore - move.packages());
|
||||
source.setStatus(positionPackages(source) == 0 ? "EMPTY" : "OCCUPIED");
|
||||
source.setVersion(value(source.getVersion()) + 1); source.setUpdatedAt(LocalDateTime.now());
|
||||
positions.updateById(source);
|
||||
|
||||
InventoryPosition target = mapper.lockTarget(source.getCustomerId(), source.getBalanceId(),
|
||||
move.targetPalletId(), source.getBatchNo());
|
||||
if (target == null) {
|
||||
target = new InventoryPosition(); target.setId(IdWorker.getId());
|
||||
target.setCustomerId(source.getCustomerId()); target.setBalanceId(source.getBalanceId());
|
||||
target.setOverseasWarehouseId(source.getOverseasWarehouseId());
|
||||
target.setDestinationWarehouseCode(source.getDestinationWarehouseCode());
|
||||
target.setPalletId(move.targetPalletId()); target.setPalletNo(move.targetPalletCode());
|
||||
target.setBatchNo(source.getBatchNo()); target.setWarehouseLocationId(move.targetLocationId());
|
||||
target.setWarehouseLocationCode(move.targetLocationCode());
|
||||
target.setAvailablePackages(0L); target.setHoldPackages(0L); target.setAllocatedPackages(0L);
|
||||
target.setPickedPackages(0L); target.setReceivingPackages(0L); target.setStatus("OCCUPIED");
|
||||
target.setVersion(0L); target.setCreatedAt(LocalDateTime.now()); target.setUpdatedAt(target.getCreatedAt());
|
||||
try { positions.insert(target); }
|
||||
catch (DuplicateKeyException duplicate) {
|
||||
target = mapper.lockTarget(source.getCustomerId(), source.getBalanceId(), move.targetPalletId(), source.getBatchNo());
|
||||
if (target == null) throw duplicate;
|
||||
}
|
||||
}
|
||||
long targetBefore = value(target.getAvailablePackages());
|
||||
target.setAvailablePackages(targetBefore + move.packages()); target.setStatus("OCCUPIED");
|
||||
target.setWarehouseLocationId(move.targetLocationId());
|
||||
target.setWarehouseLocationCode(move.targetLocationCode());
|
||||
target.setVersion(value(target.getVersion()) + 1); target.setUpdatedAt(LocalDateTime.now());
|
||||
positions.updateById(target);
|
||||
|
||||
InventoryBalance balance = balances.selectById(source.getBalanceId());
|
||||
if (balance == null) throw new IllegalStateException("库存余额不存在");
|
||||
ledger(identity, operation, balance, source, key + ":" + source.getId() + ":OUT",
|
||||
sourceBefore, -move.packages(), sourceBefore - move.packages(), move.sourcePalletCode(), move.targetPalletCode(), reason);
|
||||
ledger(identity, operation, balance, target, key + ":" + source.getId() + ":IN",
|
||||
targetBefore, move.packages(), targetBefore + move.packages(), move.sourcePalletCode(), move.targetPalletCode(), reason);
|
||||
}
|
||||
|
||||
private void ledger(PdaWarehouseIdentity identity, PdaPalletOperation operation, InventoryBalance balance,
|
||||
InventoryPosition position, String idempotencyKey, long before, long delta, long after,
|
||||
String sourcePallet, String targetPallet, String reason) {
|
||||
InventoryLedger ledger = new InventoryLedger(); ledger.setId(IdWorker.getId());
|
||||
ledger.setCustomerId(balance.getCustomerId()); ledger.setBalanceId(balance.getId());
|
||||
ledger.setPositionId(position.getId()); ledger.setOverseasWarehouseId(balance.getOverseasWarehouseId());
|
||||
ledger.setDestinationWarehouseCode(balance.getDestinationWarehouseCode());
|
||||
ledger.setInboundOrderId(balance.getInboundOrderId()); ledger.setPalletId(position.getPalletId());
|
||||
ledger.setPalletNo(position.getPalletNo()); ledger.setBatchNo(position.getBatchNo());
|
||||
ledger.setSkuCode(balance.getSkuCode()); ledger.setWarehouseLocationCode(position.getWarehouseLocationCode());
|
||||
ledger.setBusinessType(operation.getOperationType()); ledger.setBusinessOrderId(operation.getId());
|
||||
ledger.setBusinessOrderNo(String.valueOf(operation.getId())); ledger.setInboundCode(balance.getInboundCode());
|
||||
ledger.setInboundOrderNo(balance.getInboundOrderNo()); ledger.setIdempotencyKey(idempotencyKey);
|
||||
ledger.setStatusFrom("PALLET:" + sourcePallet); ledger.setStatusTo("PALLET:" + targetPallet);
|
||||
ledger.setBeforePackages(before); ledger.setDeltaPackages(delta); ledger.setAfterPackages(after);
|
||||
ledger.setOperatorId(identity.operatorKey()); ledger.setRemark(trim(reason));
|
||||
ledger.setOccurredAt(LocalDateTime.now()); ledger.setCreatedAt(ledger.getOccurredAt()); ledgers.insert(ledger);
|
||||
}
|
||||
|
||||
private Plan plan(long warehouseId, Map<String, PalletPhysicalGateway.PalletReference> pallets,
|
||||
List<PlannedMove> moves, Set<Long> targets, boolean unbind) {
|
||||
Map<Long, Long> movedBySource = new HashMap<>();
|
||||
moves.forEach(move -> movedBySource.merge(move.sourcePalletId(), move.packages(), Long::sum));
|
||||
List<PalletChange> changes = new ArrayList<>();
|
||||
for (PalletPhysicalGateway.PalletReference pallet : pallets.values()) {
|
||||
long total = mapper.palletPackages(String.valueOf(warehouseId), pallet.palletId());
|
||||
long moved = movedBySource.getOrDefault(pallet.palletId(), 0L);
|
||||
String targetStatus = targets.contains(pallet.palletId()) ? "OCCUPIED"
|
||||
: moved > 0 && moved == total ? "IDLE" : pallet.status();
|
||||
if ("SCRAPPED".equals(targetStatus)) throw conflict("已报废托盘不能执行作业");
|
||||
changes.add(new PalletChange(pallet, targetStatus, pallet.locationId()));
|
||||
}
|
||||
if (unbind && changes.stream().noneMatch(change -> "IDLE".equals(change.targetStatus()))) {
|
||||
throw conflict("解绑必须清空来源托盘");
|
||||
}
|
||||
changes.sort(Comparator.comparingLong(change -> change.pallet().palletId()));
|
||||
return new Plan(pallets, moves, changes);
|
||||
}
|
||||
|
||||
private List<PlannedMove> plannedLines(long warehouseId, List<PalletOperationRequests.MoveLine> lines,
|
||||
Map<String, PalletPhysicalGateway.PalletReference> sources,
|
||||
PalletPhysicalGateway.PalletReference target,
|
||||
PalletPhysicalGateway.PalletReference requiredSource) {
|
||||
Map<Long, PalletInventoryRow> available = new HashMap<>();
|
||||
for (PalletPhysicalGateway.PalletReference source : sources.values()) {
|
||||
for (PalletInventoryRow row : mapper.content(String.valueOf(warehouseId), source.palletId())) {
|
||||
available.put(row.getPositionId(), row);
|
||||
}
|
||||
}
|
||||
List<PlannedMove> result = new ArrayList<>();
|
||||
for (PalletOperationRequests.MoveLine line : lines) {
|
||||
PalletInventoryRow row = available.get(line.positionId());
|
||||
if (row == null || requiredSource != null && !Objects.equals(row.getPalletId(), requiredSource.palletId())) {
|
||||
throw conflict("库存行不属于已扫描的来源托盘");
|
||||
}
|
||||
if (value(row.getVersion()) != line.expectedPositionVersion()) throw conflict("库存版本已变化,请重新扫码");
|
||||
requireMovable(row, line.packages());
|
||||
result.add(new PlannedMove(row.getPositionId(), line.packages(), line.expectedPositionVersion(),
|
||||
row.getPalletId(), row.getPalletNo(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(warehouseId, target.locationId())));
|
||||
}
|
||||
requireUniqueLines(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void requireCommonDestination(long warehouseId, List<PlannedMove> moves,
|
||||
List<PalletPhysicalGateway.PalletReference> targets) {
|
||||
LinkedHashSet<String> destinations = new LinkedHashSet<>();
|
||||
for (PlannedMove move : moves) {
|
||||
PalletInventoryRow row = mapper.content(String.valueOf(warehouseId), move.sourcePalletId()).stream()
|
||||
.filter(value -> Objects.equals(value.getPositionId(), move.positionId())).findFirst()
|
||||
.orElseThrow(() -> conflict("库存行不存在"));
|
||||
destinations.add(text(row.getDestinationWarehouseCode()));
|
||||
}
|
||||
for (PalletPhysicalGateway.PalletReference target : targets) {
|
||||
mapper.content(String.valueOf(warehouseId), target.palletId()).stream()
|
||||
.map(PalletInventoryRow::getDestinationWarehouseCode).map(PdaPalletOperationService::text)
|
||||
.forEach(destinations::add);
|
||||
}
|
||||
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);
|
||||
return resolvePallets(warehouseId, values);
|
||||
}
|
||||
private Map<String, PalletPhysicalGateway.PalletReference> resolvePallets(long warehouseId,
|
||||
java.util.Collection<PalletOperationRequests.PalletExpectation> values) {
|
||||
LinkedHashMap<String, PalletPhysicalGateway.PalletReference> result = new LinkedHashMap<>();
|
||||
for (PalletOperationRequests.PalletExpectation expected : values) {
|
||||
String code = code(expected.palletCode());
|
||||
if (result.containsKey(code)) throw badRequest("一次作业不能重复提交同一托盘");
|
||||
PalletPhysicalGateway.PalletReference pallet = requirePallet(warehouseId, code);
|
||||
if (pallet.version() != expected.expectedVersion()) throw conflict("托盘版本已变化,请重新扫码");
|
||||
result.put(code, pallet);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private PalletPhysicalGateway.PalletReference requirePallet(long warehouseId, String code) {
|
||||
PalletPhysicalGateway.PalletReference pallet = physical.resolve(code(code));
|
||||
if (pallet.warehouseId() != warehouseId) throw conflict("托盘不属于 Token 冻结的海外仓");
|
||||
if ("SCRAPPED".equals(pallet.status())) throw conflict("已报废托盘不能执行作业");
|
||||
return pallet;
|
||||
}
|
||||
|
||||
private List<PalletInventoryRow> nonEmptyContent(long warehouseId, long palletId) {
|
||||
List<PalletInventoryRow> values = mapper.content(String.valueOf(warehouseId), palletId);
|
||||
if (values.isEmpty()) throw conflict("来源托盘没有可操作库存");
|
||||
return values;
|
||||
}
|
||||
|
||||
private PalletOperationResponses.PalletContent content(long warehouseId,
|
||||
PalletPhysicalGateway.PalletReference pallet) {
|
||||
List<PalletInventoryRow> rows = mapper.content(String.valueOf(warehouseId), pallet.palletId());
|
||||
LinkedHashSet<String> destinations = new LinkedHashSet<>();
|
||||
List<PalletOperationResponses.InventoryLine> inventory = rows.stream().map(row -> {
|
||||
destinations.add(text(row.getDestinationWarehouseCode()));
|
||||
return new PalletOperationResponses.InventoryLine(row.getCustomerId(), row.getCustomerName(),
|
||||
value(row.getBalanceId()), value(row.getPositionId()), row.getInboundCode(), row.getSkuCode(),
|
||||
first(row.getProductNameZh(), row.getProductNameEn()), value(row.getAvailablePackages()),
|
||||
value(row.getHoldPackages()), value(row.getAllocatedPackages()), value(row.getPickedPackages()),
|
||||
value(row.getReceivingPackages()), value(row.getVersion()));
|
||||
}).toList();
|
||||
long total = rows.stream().mapToLong(PdaPalletOperationService::rowPackages).sum();
|
||||
String locationCode = locationCode(warehouseId, pallet.locationId());
|
||||
return new PalletOperationResponses.PalletContent(pallet.palletId(), pallet.palletCode(), pallet.status(),
|
||||
pallet.version(), pallet.locationId(), locationCode, destinations.size() == 1 ? destinations.iterator().next() : null,
|
||||
total, inventory);
|
||||
}
|
||||
|
||||
private PdaPalletOperation begin(PdaWarehouseIdentity identity, String type, String key, String requestHash) {
|
||||
PdaPalletOperation existing = mapper.findOperation(identity.operatorKey(), key);
|
||||
if (existing != null) return validateExisting(existing, type, requestHash);
|
||||
try {
|
||||
return newTransaction.execute(status -> {
|
||||
PdaPalletOperation operation = new PdaPalletOperation(); operation.setId(IdWorker.getId());
|
||||
operation.setOverseasWarehouseId(identity.overseasWarehouseId());
|
||||
operation.setOperatorKey(identity.operatorKey()); operation.setIdempotencyKey(key);
|
||||
operation.setOperationType(type); operation.setRequestHash(requestHash); operation.setStatus("PROCESSING");
|
||||
operation.setCreatedAt(LocalDateTime.now()); operation.setUpdatedAt(operation.getCreatedAt());
|
||||
mapper.insert(operation); return operation;
|
||||
});
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
PdaPalletOperation winner = mapper.findOperation(identity.operatorKey(), key);
|
||||
if (winner != null) return validateExisting(winner, type, requestHash);
|
||||
throw duplicate;
|
||||
}
|
||||
}
|
||||
|
||||
private PdaPalletOperation validateExisting(PdaPalletOperation value, String type, String requestHash) {
|
||||
if (!type.equals(value.getOperationType()) || !requestHash.equals(value.getRequestHash())) {
|
||||
throw conflict("幂等键已被不同托盘作业使用");
|
||||
}
|
||||
if (!"COMPLETED".equals(value.getStatus())) {
|
||||
throw conflict("同一托盘作业正在处理或需要人工恢复");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void saveTransitions(long operationId, List<PalletPhysicalGateway.Transition> transitions) {
|
||||
newTransaction.executeWithoutResult(status -> {
|
||||
PdaPalletOperation operation = mapper.selectById(operationId);
|
||||
operation.setRemoteTransitionsJson(write(transitions)); operation.setUpdatedAt(LocalDateTime.now());
|
||||
mapper.updateById(operation);
|
||||
});
|
||||
}
|
||||
private void fail(long operationId, RuntimeException failure, boolean compensated) {
|
||||
newTransaction.executeWithoutResult(status -> {
|
||||
PdaPalletOperation operation = mapper.selectById(operationId);
|
||||
if (operation == null || "COMPLETED".equals(operation.getStatus())) return;
|
||||
operation.setStatus(compensated ? "FAILED_COMPENSATED" : "COMPENSATION_REQUIRED");
|
||||
operation.setLastError(limit(failure.getMessage(), 2000)); operation.setUpdatedAt(LocalDateTime.now());
|
||||
mapper.updateById(operation);
|
||||
});
|
||||
}
|
||||
private boolean compensate(long warehouseId, List<PalletPhysicalGateway.Transition> transitions) {
|
||||
boolean success = true;
|
||||
for (int i = transitions.size() - 1; i >= 0; i--) {
|
||||
try { physical.compensate(warehouseId, transitions.get(i)); }
|
||||
catch (RuntimeException ignored) { success = false; }
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private PalletOperationResponses.Operation read(PdaPalletOperation operation) {
|
||||
try { return objectMapper.readValue(operation.getResultJson(), PalletOperationResponses.Operation.class); }
|
||||
catch (JsonProcessingException exception) { throw new IllegalStateException("无法读取托盘作业幂等结果", exception); }
|
||||
}
|
||||
private String write(Object value) {
|
||||
try { return objectMapper.writeValueAsString(value); }
|
||||
catch (JsonProcessingException exception) { throw new IllegalStateException("无法序列化托盘作业", exception); }
|
||||
}
|
||||
private String hash(Object value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(write(value).getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
|
||||
}
|
||||
private static String transitionKey(String operator, String key, long palletId) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest((operator + "|" + key + "|" + palletId).getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
|
||||
}
|
||||
private String locationCode(long warehouseId, long locationId) {
|
||||
PalletPhysicalGateway.LocationReference location = physical.resolveLocation(warehouseId, locationId);
|
||||
if (location == null || location.locationId() != locationId || "INACTIVE".equals(location.status())) {
|
||||
throw conflict("托盘库位不存在或已停用");
|
||||
}
|
||||
return location.locationCode();
|
||||
}
|
||||
private static void requireMovable(PalletInventoryRow row, long packages) {
|
||||
if (packages <= 0 || value(row.getAvailablePackages()) < packages) throw conflict("可用库存箱数不足");
|
||||
if (value(row.getHoldPackages()) > 0 || value(row.getAllocatedPackages()) > 0
|
||||
|| value(row.getPickedPackages()) > 0 || value(row.getReceivingPackages()) > 0) {
|
||||
throw conflict("冻结、已分配、已拣或收货中的库存不能执行托盘作业");
|
||||
}
|
||||
}
|
||||
private static void requireUniqueLines(List<PlannedMove> moves) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
if (moves.stream().anyMatch(move -> !ids.add(move.positionId()))) throw badRequest("库存行不能重复提交");
|
||||
}
|
||||
private static long rowPackages(PalletInventoryRow row) {
|
||||
return value(row.getAvailablePackages()) + value(row.getHoldPackages()) + value(row.getAllocatedPackages())
|
||||
+ value(row.getPickedPackages()) + value(row.getReceivingPackages());
|
||||
}
|
||||
private static long positionPackages(InventoryPosition value) {
|
||||
return value(value.getAvailablePackages()) + value(value.getHoldPackages()) + value(value.getAllocatedPackages())
|
||||
+ value(value.getPickedPackages()) + value(value.getReceivingPackages());
|
||||
}
|
||||
private static String businessKey(Plan plan) {
|
||||
return plan.pallets().values().stream().map(PalletPhysicalGateway.PalletReference::palletCode)
|
||||
.sorted().collect(java.util.stream.Collectors.joining("->"));
|
||||
}
|
||||
private static String required(String value, String message) { String result = trim(value); if (result == null) throw badRequest(message); return result; }
|
||||
private static String code(String value) { return required(value, "托盘码不能为空").toUpperCase(); }
|
||||
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
|
||||
private static String text(String value) { return value == null ? "" : value.trim(); }
|
||||
private static String first(String first, String second) { String value = trim(first); return value == null ? trim(second) : value; }
|
||||
private static String limit(String value, int size) { if (value == null) return null; return value.length() <= size ? value : value.substring(0, size); }
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
|
||||
private record PlannedMove(long positionId, long packages, long expectedVersion,
|
||||
long sourcePalletId, String sourcePalletCode,
|
||||
long targetPalletId, String targetPalletCode, long targetLocationId,
|
||||
String targetLocationCode) { }
|
||||
private record PalletChange(PalletPhysicalGateway.PalletReference pallet, String targetStatus,
|
||||
long targetLocationId) { }
|
||||
private record Plan(Map<String, PalletPhysicalGateway.PalletReference> pallets,
|
||||
List<PlannedMove> moves, List<PalletChange> changes) { }
|
||||
}
|
||||
Reference in New Issue
Block a user