装柜修改

This commit is contained in:
2026-08-11 20:00:31 +08:00
parent 48bb752007
commit bed917ba6c
13 changed files with 96 additions and 34 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ baiyue.security:
internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long}
token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth}
issuer: ${JWT_ISSUER:auth-service}
access-token-ttl: ${ACCESS_TOKEN_TTL:30m}
access-token-ttl: ${ACCESS_TOKEN_TTL:24h}
refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d}
max-login-failures: ${MAX_LOGIN_FAILURES:5}
lock-duration: ${LOGIN_LOCK_DURATION:15m}
@@ -247,7 +247,7 @@ public class InboundOrderRepository {
Function.identity(), (left, right) -> left));
}
private Map<Long, String> locationCodesMap(Long tenantId, Map<Long, Long> tenantByInbound) {
public Map<Long, String> locationCodesMap(Long tenantId, Map<Long, Long> tenantByInbound) {
if (tenantByInbound.isEmpty()) {
return Map.of();
}
@@ -33,10 +33,13 @@ public class InboundPalletAllocationRepository {
.eq(InboundPalletAllocationEntity::getInboundId, inboundId));
LocalDateTime now = LocalDateTime.now();
int allocationCount = result.allocations().size();
int perAllocation = allocationCount == 0 ? 0
: (int) Math.ceil((double) totalPackageCount / allocationCount);
int remaining = totalPackageCount;
for (var source : result.allocations()) {
if (allocationCount > totalPackageCount) {
throw new IllegalArgumentException("托盘数量不能超过入库单总箱数");
}
int basePackageCount = allocationCount == 0 ? 0 : totalPackageCount / allocationCount;
int remainder = allocationCount == 0 ? 0 : totalPackageCount % allocationCount;
for (int index = 0; index < result.allocations().size(); index++) {
var source = result.allocations().get(index);
InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
value.setTenantId(tenantId);
value.setInboundId(inboundId);
@@ -47,10 +50,9 @@ public class InboundPalletAllocationRepository {
value.setPalletCode(source.palletCode());
value.setPalletCapacityCbm(capacity);
value.setAllocatedVolumeCbm(source.allocatedVolumeCbm());
int allocatedPackages = Math.min(perAllocation, remaining);
int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0);
value.setAllocatedPackageCount(allocatedPackages);
value.setPickedPackageCount(0);
remaining -= allocatedPackages;
value.setLocationId(source.locationId());
value.setLocationCode(source.locationCode());
value.setReservationId(source.reservationId());
@@ -7,6 +7,7 @@ import com.baiyue.firstleg.entity.InboundOutboxEntity;
import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -32,19 +33,19 @@ public class FinanceFeeSourceOutboxRelay {
FinanceFeeSourceOutboxService.ADJUSTED,
FinanceFeeSourceOutboxService.VOIDED);
private final FinanceFeeSourceOutboxRepository repository;
private final FinanceFeeSourceEventClient client;
private final ObjectMapper objectMapper;
private final Clock clock;
private final String workerId;
@Resource
private FinanceFeeSourceOutboxRepository repository;
@Resource
private FinanceFeeSourceEventClient client;
@Resource
private ObjectMapper objectMapper;
private Clock clock = Clock.systemDefaultZone();
private String workerId = UUID.randomUUID().toString();
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
@Autowired
public FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository,
FinanceFeeSourceEventClient client,
ObjectMapper objectMapper) {
this(repository, client, objectMapper, Clock.systemDefaultZone(), UUID.randomUUID().toString());
public FinanceFeeSourceOutboxRelay() {
}
FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository,
@@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
public class MeasurementLocationOccupationService {
@@ -38,7 +39,10 @@ public class MeasurementLocationOccupationService {
long warehouseId = requireWarehouse(order);
long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID;
BigDecimal volume = requireVolume(order);
String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion;
// 补偿释放后的预占记录需要保留审计,失败重试必须使用新的物理预占键;
// 若仍存在活动预占,仓储侧会按业务单据复用,不会创建新的资源占用。
String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion
+ "-" + UUID.randomUUID();
WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve(
internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId,
SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume,
@@ -79,7 +79,7 @@ public class UnifiedInboundQueryService {
}
InboundTransportSnapshotEntity transport = includePhaseThree
? operations.findTransport(tenantId, inbound.getId()) : null;
return new UnifiedInboundDetail(summary(inbound, detail, preload, pick, evidence, deliveryAt, transport), timeline);
return new UnifiedInboundDetail(summary(inbound, detail, preload, pick, evidence, deliveryAt, transport, null), timeline);
}
private List<UnifiedInboundSummary> summaries(List<InboundOrderEntity> records) {
@@ -92,17 +92,20 @@ public class UnifiedInboundQueryService {
var evidences = operations.findEvidence(phaseThreeOwners);
var deliveries = operations.findDeliveryHistory(phaseThreeOwners);
var transports = operations.findTransports(phaseThreeOwners);
Map<Long, Long> owners = records.stream().collect(Collectors.toMap(
InboundOrderEntity::getId, InboundOrderEntity::getTenantId, (left, right) -> left));
var locationCodes = inbounds.locationCodesMap(null, owners);
return records.stream().map(inbound -> summary(inbound, null, preloads.get(inbound.getId()),
picks.get(inbound.getId()), evidences.getOrDefault(inbound.getId(), List.of()),
deliveries.get(inbound.getId()) == null ? null : deliveries.get(inbound.getId()).getOccurredAt(),
transports.get(inbound.getId()))).toList();
transports.get(inbound.getId()), locationCodes.get(inbound.getId()))).toList();
}
private UnifiedInboundSummary summary(InboundOrderEntity inbound, InboundOrderDetail detail,
com.baiyue.firstleg.split.entity.PreloadPlanEntity preload,
com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity pick,
List<LoadingEvidenceEntity> evidence, LocalDateTime deliveryAt,
InboundTransportSnapshotEntity transport) {
InboundTransportSnapshotEntity transport, String locationCodes) {
String mainStatus = mainStatus(inbound.getProcessStatus());
List<LoadingEvidenceEntity> activeEvidence = evidence.stream()
.filter(item -> item.getStatus() == null || "ACTIVE".equals(item.getStatus())).toList();
@@ -121,7 +124,7 @@ public class UnifiedInboundQueryService {
"DELIVERED".equals(mainStatus) ? "COMPLETED" : "PENDING", deliveryAt,
inbound.getExpressTrackingNo()) : null;
var inboundSummary = detail == null
? com.baiyue.firstleg.dto.InboundOrderSummary.from(inbound, transport, null) : detail.summary();
? com.baiyue.firstleg.dto.InboundOrderSummary.from(inbound, transport, locationCodes) : detail.summary();
return new UnifiedInboundSummary(inboundSummary, mainStatus,
nextAction(mainStatus, inbound.getProcessStatus(), preload == null ? null : preload.getStatus()),
preloadSummary, loadingSummary, deliverySummary);
@@ -58,7 +58,7 @@ class MeasurementLocationOccupationServiceTest {
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND);
assertThat(reserve.getValue().idempotencyKey()).isEqualTo("measure-88-30-3");
assertThat(reserve.getValue().idempotencyKey()).startsWith("measure-88-30-3-");
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
@@ -46,7 +46,8 @@ public interface FirstlegUnifiedOrderClient {
Integer version,
Long customerId, String customerName, Long warehouseId, String warehouseName,
Long deliveryWarehouseId, String deliveryWarehouseName, String channelName,
Integer totalPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
Integer totalPackageCount, String locationCodes,
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
BigDecimal chargeableWeightKg, LocalDateTime measureCompletedAt,
String loadingStatus, String containerNo, String cabinetNo, String vesselName, String voyageNo,
LocalDateTime sailingAt, LocalDateTime portArrivalAt,
@@ -18,7 +18,8 @@ public final class UnifiedOrderResponse {
Long receivingWarehouseId, String receivingWarehouseName,
Long destinationWarehouseId, String destinationWarehouseName,
Long inboundId, Integer inboundVersion, String inboundCode, LocalDate expectedShipDate,
Integer totalPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
Integer totalPackageCount, String recommendedPalletType, String locationCodes,
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
MeasurementSummary measurement, PreloadSummary preload,
LoadingSummary loading, DeliverySummary delivery, TransportSummary transport,
String customerRemark, String latestInternalNote,
@@ -140,7 +140,7 @@ public class UnifiedOrderQueryService {
order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null,
order.getInboundCode(),
order.getExpectedShipDate(), null, null, null, null, null, null, null, null,
order.getExpectedShipDate(), null, null, null, null, null, null, null, null, null, null,
order.getCustomerRemark(), latestInternalNote,
order.getCreatedAt(), order.getUpdatedAt());
}
@@ -149,8 +149,9 @@ public class UnifiedOrderQueryService {
summary.mainStatus(), summary.nextAction(), order.getStatus(), order.getCustomerId(), order.getCustomerName(),
order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(),
order.getExpectedShipDate(), inbound.totalPackageCount(), inbound.actualGrossWeightKg(),
inbound.actualVolumeCbm(), measurement(inbound), preload(summary.preload()), loading(summary.loading()),
order.getExpectedShipDate(), inbound.totalPackageCount(), "STANDARD", inbound.locationCodes(),
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(), measurement(inbound),
preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), order.getCustomerRemark(), latestInternalNote,
order.getCreatedAt(), order.getUpdatedAt());
}
@@ -162,7 +163,8 @@ public class UnifiedOrderQueryService {
summary.mainStatus(), summary.nextAction(), parseOrderStatus(inbound.orderStatusSnapshot()),
inbound.customerId(), inbound.customerName(), inbound.channelName(), inbound.warehouseId(),
inbound.warehouseName(), inbound.deliveryWarehouseId(), inbound.deliveryWarehouseName(),
inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.totalPackageCount(), inbound.actualGrossWeightKg(),
inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.totalPackageCount(),
"STANDARD", inbound.locationCodes(), inbound.actualGrossWeightKg(),
inbound.actualVolumeCbm(), measurement(inbound), preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), order == null ? null : order.getCustomerRemark(), latestInternalNote,
inbound.createdAt(), inbound.updatedAt());
@@ -136,6 +136,8 @@ class UnifiedOrderQueryServiceTest {
assertThat(row.loading().loadingVersion()).isEqualTo(3);
assertThat(row.loading().pickingVersion()).isEqualTo(3);
assertThat(row.inboundCode()).isEqualTo("RK-77");
assertThat(row.recommendedPalletType()).isEqualTo("STANDARD");
assertThat(row.locationCodes()).isEqualTo("A0-01, B2-03");
});
verify(orders, never()).unifiedPage(any(), any(), any());
}
@@ -244,7 +246,7 @@ class UnifiedOrderQueryServiceTest {
private FirstlegUnifiedOrderClient.Summary summary(String status, String action) {
var inbound = new FirstlegUnifiedOrderClient.Inbound(
88L, 10001L, 77L, "FL-77", "RK-77", "INBOUNDED", "APPROVED", "APPROVED", 7,
30001L, "测试客户", 11L, "深圳仓", 22L, "ONT8", "美森", 10, null, null, null,
30001L, "测试客户", 11L, "深圳仓", 22L, "ONT8", "美森", 10, "A0-01, B2-03", null, null, null,
null, null, null, null, null, null, null, null, null,
LocalDateTime.of(2026, 8, 6, 10, 0), LocalDateTime.of(2026, 8, 6, 11, 0));
return new FirstlegUnifiedOrderClient.Summary(inbound, status, action,
@@ -108,13 +108,15 @@ public class PalletLocationAllocationService {
if (active.size() != split.palletCount()) {
throw new BizException(ResultCode.CONFLICT, "活动预占数量与托盘拆分数量不一致");
}
if (active.size() > request.packageCount()) {
throw new BizException(ResultCode.CONFLICT, "托盘数量超过入库单总箱数,无法为每个托盘分配至少一箱");
}
active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId));
List<LocationDtos.OccupancyTarget> targets = new ArrayList<>(active.size());
int perAllocation = (int) Math.ceil((double) request.packageCount() / active.size());
int remainingPackages = request.packageCount();
int basePackageCount = request.packageCount() / active.size();
int remainder = request.packageCount() % active.size();
for (int index = 0; index < active.size(); index++) {
int allocatedPackages = Math.min(perAllocation, remainingPackages);
remainingPackages -= allocatedPackages;
int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0);
targets.add(new LocationDtos.OccupancyTarget(active.get(index).reservationId(),
split.allocations().get(index).allocatedVolumeCbm(), allocatedPackages));
}
@@ -21,6 +21,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -140,6 +141,49 @@ class PalletLocationAllocationServiceTest {
.isEqualTo(20);
}
@Test
void confirmDistributesRemainderWithoutZeroPackageAllocations() {
List<LocationDtos.ActiveResource> resources = new ArrayList<>();
List<LocationDtos.OccupancyResult> occupancyResults = new ArrayList<>();
for (int index = 1; index <= 10; index++) {
long reservationId = 100L + index;
long palletId = 200L + index;
long locationId = 300L + index;
resources.add(new LocationDtos.ActiveResource(reservationId, null, 20L, palletId,
"TP-" + index, locationId, "A-" + index, false, "RESERVED"));
occupancyResults.add(new LocationDtos.OccupancyResult(400L + index, "OC-" + index,
reservationId, palletId, locationId, "A-" + index, false));
when(pallets.transitionStatus(10001L, 20L, palletId, List.of("RESERVED"), "OCCUPIED"))
.thenReturn(1);
}
when(locations.findActiveResources(10001L, "FIRSTLEG", 89L)).thenReturn(resources);
when(locations.confirmOccupied(any())).thenReturn(occupancyResults);
service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L, "FIRSTLEG", 89L,
9001L, new BigDecimal("12.0"), new BigDecimal("1.2"), 12));
org.mockito.ArgumentCaptor<LocationDtos.ConfirmCommand> command =
org.mockito.ArgumentCaptor.forClass(LocationDtos.ConfirmCommand.class);
verify(locations).confirmOccupied(command.capture());
assertThat(command.getValue().targets())
.extracting(LocationDtos.OccupancyTarget::allocatedPackageCount)
.containsExactly(2, 2, 1, 1, 1, 1, 1, 1, 1, 1);
}
@Test
void confirmRejectsMorePalletsThanPackages() {
List<LocationDtos.ActiveResource> resources = List.of(
new LocationDtos.ActiveResource(101L, null, 20L, 30L, "TP-001", 40L, "A-01", false, "RESERVED"),
new LocationDtos.ActiveResource(102L, null, 20L, 31L, "TP-002", 41L, "A-02", false, "RESERVED"));
when(locations.findActiveResources(10001L, "FIRSTLEG", 90L)).thenReturn(resources);
assertThatThrownBy(() -> service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L,
"FIRSTLEG", 90L, 9001L, new BigDecimal("2.0"), new BigDecimal("1.0"), 1)))
.isInstanceOf(BizException.class)
.hasMessageContaining("托盘数量超过入库单总箱数");
verify(locations, never()).confirmOccupied(any());
}
@Test
void shouldReleaseOnlyPalletsReturnedBySelectedLocationRelease() {
var resource = new LocationDtos.ExactSelectiveReleaseResult(