This commit is contained in:
2026-08-27 14:30:23 +08:00
parent 3b38fdd33d
commit 0a3ec0b8a6
10 changed files with 283 additions and 46 deletions
@@ -6473,9 +6473,7 @@ ON DUPLICATE KEY UPDATE
location_type = VALUES(location_type),
max_weight_kg = VALUES(max_weight_kg),
shared_overflow = VALUES(shared_overflow),
status = 'AVAILABLE',
remark = VALUES(remark),
deleted = 0;
remark = VALUES(remark);
INSERT INTO wh_location (
id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name,
@@ -6494,9 +6492,7 @@ ON DUPLICATE KEY UPDATE
location_type = VALUES(location_type),
max_weight_kg = VALUES(max_weight_kg),
shared_overflow = VALUES(shared_overflow),
status = 'AVAILABLE',
remark = VALUES(remark),
deleted = 0;
remark = VALUES(remark);
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
DELIMITER //
@@ -18,6 +18,8 @@ import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
@@ -32,6 +34,7 @@ import java.util.stream.Collectors;
/** PDA 任务结算:按柜次生成实装/余量履约分片,并结算测量与库位资源。 */
@Service
public class PdaPickSettlementService {
private static final Logger log = LoggerFactory.getLogger(PdaPickSettlementService.class);
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
private static final String SOURCE_SERVICE = "firstleg-service";
@@ -379,7 +382,7 @@ public class PdaPickSettlementService {
private static List<Long> releasedReservationIds(List<InboundPalletAllocationEntity> allocations) {
return allocations.stream().filter(item -> remaining(item) == 0).map(PdaPickSettlementService::reservationId)
.sorted().toList();
.distinct().sorted().toList();
}
private static List<WarehousePalletLocationClient.SettlementTransferItem> remainingTransferItems(
@@ -422,18 +425,35 @@ public class PdaPickSettlementService {
operatorId, operatorName, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
releasedReservationIds, remainingItems));
} catch (FeignException exception) {
log.warn("PDA warehouse settlement transport failure taskId={} sourceInboundId={} operationId={} "
+ "httpStatus={} releasedReservationIds={} remainingReservations={}",
taskId, parentInboundId, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
exception.status(), releasedReservationIds, settlementTransferFacts(remainingItems));
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储服务不可用,无法结算PDA库位资源");
}
if (response == null || !response.successful() || response.data() == null
|| response.data().released() == null || response.data().transferred() == null
|| !sameReleasedReservations(releasedReservationIds, response.data().released())
|| !sameTransferredResources(remainingItems, response.data().transferred())) {
log.warn("PDA warehouse settlement rejected taskId={} sourceInboundId={} operationId={} "
+ "responseCode={} requestId={} traceId={} expectedAction=RELEASE_OR_TRANSFER "
+ "releasedReservationIds={} remainingReservations={}",
taskId, parentInboundId, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
response == null ? null : response.code(), response == null ? null : response.requestId(),
response == null ? null : response.traceId(), releasedReservationIds,
settlementTransferFacts(remainingItems));
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
throw new BizException(code, response == null || response.message() == null
? "仓储服务原子结算PDA库位资源失败" : response.message());
}
}
private static List<String> settlementTransferFacts(
List<WarehousePalletLocationClient.SettlementTransferItem> remainingItems) {
return remainingItems.stream().map(item -> item.reservationId() + ":" + item.remainingPackageCount())
.sorted().toList();
}
private long existingSettlementTarget(long sourceInboundId, String idempotencyKey) {
try {
ApiResponse<Long> response = warehouse.settlementTarget(internalToken, sourceInboundId, idempotencyKey);
@@ -9,6 +9,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
@@ -613,6 +614,62 @@ class LoadingPickServiceTest {
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
}
@Test
void warehouseSettlementConflictLeavesCompletionClaimAndDownstreamFinalizationUnwritten() {
LoadingPickTaskEntity task = task();
task.setVersion(1);
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
var owner = new LoadingPickItemMapper.InboundOwner(101L, 50L);
when(repository.inboundOwners(20L)).thenReturn(List.of(owner));
when(repository.inboundForUpdate(101L, 50L)).thenReturn(
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
"释放库位资源时发生并发变更"))
.when(settlementService).settle(20L, 9L, "operator");
assertThatThrownBy(() -> service.complete(20L,
new CompleteLoadingRequest(1, "complete-real-a0409", "PDA确认装柜完成"), 9L, "operator"))
.isInstanceOf(BizException.class)
.hasMessageContaining("释放库位资源时发生并发变更");
verify(repository, never()).approve(anyLong(), anyLong(), anyString(), anyString());
verify(repository, never()).insert(any(LoadingPickOperationEntity.class));
verify(repository, never()).syncInboundProjections(anyLong(), anyLong(), anyString());
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
verify(loadingListService, never()).publishCompletedEvent(anyLong());
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(anyLong(), anyLong(), anyLong(),
anyLong(), anyString(), anyString(), anyString());
}
@Test
void currentCompleteRequestFinishesAfterUniqueResourceSettlementSucceeds() {
LoadingPickTaskEntity task = task();
task.setVersion(1);
LoadingPickItemEntity item = item();
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.task(20L)).thenReturn(task);
when(repository.items(20L)).thenReturn(List.of(item));
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(repository.approve(20L, 9L, "operator", "PDA确认装柜完成")).thenReturn(1);
var owner = new LoadingPickItemMapper.InboundOwner(101L, 50L);
when(repository.inboundOwners(20L)).thenReturn(List.of(owner));
when(repository.inboundForUpdate(101L, 50L)).thenReturn(
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
Task result = service.complete(20L, new CompleteLoadingRequest(1,
"complete-1000001-2092817774453780481-2092787765815078913--45b8eb0887ff317c",
"PDA确认装柜完成"), 9L, "operator");
assertThat(result.id()).isEqualTo(20L);
verify(settlementService).settle(20L, 9L, "operator");
verify(repository).insert(org.mockito.ArgumentMatchers.argThat(
(LoadingPickOperationEntity operation) -> "COMPLETE_LOADING".equals(operation.getActionType())));
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
verify(loadingListService).publishCompletedEvent(10L);
}
@Test
void atomicallySubmitsCompleteBatchAndReplaysSameResult() {
LoadingPickTaskEntity task = task();
@@ -179,10 +179,13 @@ class PdaPickSettlementServiceTest {
when(splits.lockParent(31L)).thenReturn(parent(20));
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
when(allocations.lockConfirmed(10001L, 31L))
.thenReturn(List.of(allocation(1L, 1, 501L, 10), allocation(2L, 2, 502L, 10)));
InboundPalletAllocationEntity first = allocation(1L, 1, 501L, 10);
InboundPalletAllocationEntity second = allocation(2L, 2, 501L, 10);
first.setPalletId(801L);
second.setPalletId(801L);
when(allocations.lockConfirmed(10001L, 31L)).thenReturn(List.of(first, second));
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new WarehousePalletLocationClient.SettlementResult(List.of(released(91L, 501L), released(92L, 502L)),
new WarehousePalletLocationClient.SettlementResult(List.of(released(91L, 501L), released(92L, 501L)),
List.of())));
writes(0, 20);
@@ -0,0 +1,72 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
const locationService = read('../../warehouse/src/main/java/com/baiyue/warehouse/service/LocationResourceService.java')
const locationRepository = read('../../warehouse/src/main/java/com/baiyue/warehouse/repository/LocationRepository.java')
const locationMapper = read('../../warehouse/src/main/java/com/baiyue/warehouse/mapper/LocationOccupancyMapper.java')
const locationTests = read('../../warehouse/src/test/java/com/baiyue/warehouse/service/LocationResourceServiceTest.java')
const settlementService = read('../../warehouse/src/main/java/com/baiyue/warehouse/service/PalletLocationAllocationService.java')
const settlementTests = read('../../warehouse/src/test/java/com/baiyue/warehouse/service/PalletLocationAllocationServiceTest.java')
const finalizeHttp = read('phase3-loading-finalize.mjs')
function releaseDecision({ sharedOverflow, locationStatus, activeOccupanciesAfterRelease }) {
if (sharedOverflow) return 'SHARED_STATUS_UNCHANGED'
if (activeOccupanciesAfterRelease > 0) return 'KEEP_OCCUPIED'
if (locationStatus === 'AVAILABLE') return 'ALREADY_AVAILABLE'
if (locationStatus === 'OCCUPIED') return 'TRANSITION_TO_AVAILABLE'
return 'CONFLICT'
}
test('offline release decision converges active-occupancy/location drift', () => {
assert.equal(releaseDecision({ sharedOverflow: false, locationStatus: 'AVAILABLE', activeOccupanciesAfterRelease: 0 }),
'ALREADY_AVAILABLE')
assert.equal(releaseDecision({ sharedOverflow: false, locationStatus: 'OCCUPIED', activeOccupanciesAfterRelease: 0 }),
'TRANSITION_TO_AVAILABLE')
assert.equal(releaseDecision({ sharedOverflow: false, locationStatus: 'OCCUPIED', activeOccupanciesAfterRelease: 1 }),
'KEEP_OCCUPIED')
assert.equal(releaseDecision({ sharedOverflow: true, locationStatus: 'AVAILABLE', activeOccupanciesAfterRelease: 5 }),
'SHARED_STATUS_UNCHANGED')
})
test('releaseSelectedReservations accepts AVAILABLE drift only after the last active occupancy is released', () => {
const block = locationService.slice(locationService.indexOf('releaseSelectedReservations('),
locationService.indexOf('transferToChild(', locationService.indexOf('releaseSelectedReservations(')))
assert.match(block, /releaseOccupancy/)
assert.match(block, /(?:count|has|exists|remaining)[A-Za-z]*(?:Active|Occupied|Occupanc)/i)
assert.match(block, /"AVAILABLE"\.equals\(location\.getStatus\(\)\)|location\.getStatus\(\).*"AVAILABLE"/s)
assert.match(block, /"OCCUPIED"/)
assert.match(block, /changeStatus/)
assert.doesNotMatch(block,
/releaseOccupancy[\s\S]{0,400}changeStatus\([^;]+"OCCUPIED",\s*"AVAILABLE"[^;]+\)\s*!=\s*1\)\s*\{/,
'release must not unconditionally require OCCUPIED -> AVAILABLE after occupancy release')
})
test('repository can count or lock remaining active occupancies for one location', () => {
assert.match(locationRepository, /(?:count|has|exists|lock)[A-Za-z]*(?:Active|Occupied|Occupanc)[A-Za-z]*\(/i)
assert.match(locationMapper, /occupancy_status\s*=\s*'OCCUPIED'/i)
assert.match(locationMapper, /location_id/)
})
test('Warehouse unit tests lock all three non-shared convergence cases', () => {
assert.match(locationTests, /AVAILABLE[\s\S]{0,1200}(?:active occupancy|活动占用|drift|漂移)/i)
assert.match(locationTests, /(?:last|最后)[\s\S]{0,1200}(?:AVAILABLE|changeStatus)/i)
assert.match(locationTests, /(?:other|remaining|其他|仍有)[\s\S]{0,1200}(?:occupancy|占用)[\s\S]{0,600}(?:never\(\)\.changeStatus|OCCUPIED)/i)
})
test('same settlement key replays without a second release or transfer', () => {
assert.match(settlementService, /"COMPLETED"\.equals\(operation\.getOperationStatus\(\)\)[\s\S]{0,180}replaySettlementResult/)
assert.match(settlementTests, /shouldReplayCompletedPdaSettlementForSameIdempotencyKey/)
assert.match(settlementTests, /verify\(locations,\s*never\(\)\)\.releaseSelectedReservations/)
assert.match(settlementTests, /verify\(locations,\s*never\(\)\)\.transferToChildByReservation/)
})
test('HTTP failure contract proves task plan allocation location history outbox operation zero drift', () => {
for (const fact of ['task', 'plan', 'inbound', 'allocation', 'location', 'history', 'outbox', 'operation']) {
assert.match(finalizeHttp, new RegExp(fact, 'i'), `missing ${fact} failure baseline`)
}
assert.match(finalizeHttp, /A0409/)
assert.match(finalizeHttp, /零写入|零漂移/)
})
@@ -109,6 +109,16 @@ public interface LocationMapper extends BaseMapper<LocationEntity> {
@Param("warehouseId") long warehouseId,
@Param("limit") int limit);
@Select("""
SELECT * FROM wh_location
WHERE id = #{id} AND tenant_id = #{tenantId} AND warehouse_id = #{warehouseId}
AND deleted = 0
FOR UPDATE
""")
LocationEntity lockOwned(@Param("tenantId") long tenantId,
@Param("warehouseId") long warehouseId,
@Param("id") long id);
@Insert("""
INSERT INTO wh_location (
id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name,
@@ -61,6 +61,16 @@ public interface LocationReservationMapper extends BaseMapper<LocationReservatio
int release(@Param("tenantId") long tenantId, @Param("id") long id,
@Param("version") int version, @Param("reason") String reason);
@Update("""
UPDATE wh_location_reservation
SET reservation_status = 'RELEASED', released_at = CURRENT_TIMESTAMP(3),
release_reason = #{reason}, updated_at = CURRENT_TIMESTAMP(3), version = version + 1
WHERE id = #{id} AND tenant_id = #{tenantId}
AND reservation_status = 'CONFIRMED' AND version = #{version}
""")
int releaseConfirmed(@Param("tenantId") long tenantId, @Param("id") long id,
@Param("version") int version, @Param("reason") String reason);
@Update("""
UPDATE wh_location_reservation
SET business_id = #{targetBusinessId}, inbound_code = #{targetInboundCode},
@@ -44,6 +44,10 @@ public class LocationRepository {
.eq(LocationEntity::getId, id));
}
public LocationEntity lockOwned(long tenantId, long warehouseId, long id) {
return locations.lockOwned(tenantId, warehouseId, id);
}
public PageResult<LocationEntity> page(Long tenantId, PageQuery query) {
Page<LocationEntity> result = locations.selectLocationPage(new Page<>(query.page(), query.size()),
tenantId, query.warehouseId(), query.zoneCode(), query.locationCode(), query.locationType(), query.status());
@@ -94,6 +98,10 @@ public class LocationRepository {
|| occupancies.countActiveByLocation(tenantId, locationId) > 0;
}
public int countActiveOccupancies(long tenantId, long locationId) {
return occupancies.countActiveByLocation(tenantId, locationId);
}
public List<LocationReservationEntity> findReservationsByKeys(long tenantId, String sourceService,
Collection<String> keys) {
if (keys.isEmpty()) {
@@ -129,6 +137,10 @@ public class LocationRepository {
return reservations.release(entity.getTenantId(), entity.getId(), entity.getVersion(), reason);
}
public int releaseConfirmedReservation(LocationReservationEntity entity, String reason) {
return reservations.releaseConfirmed(entity.getTenantId(), entity.getId(), entity.getVersion(), reason);
}
public int transferConfirmedReservation(LocationReservationEntity entity, String sourceService,
long sourceBusinessId, long targetBusinessId,
String targetInboundCode) {
@@ -24,6 +24,8 @@ import com.baiyue.warehouse.entity.LocationReservationEntity;
import com.baiyue.warehouse.entity.WarehouseEntity;
import com.baiyue.warehouse.repository.LocationRepository;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -42,6 +44,7 @@ import java.util.LinkedHashMap;
import java.util.stream.Collectors;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
@@ -51,6 +54,8 @@ import java.util.Set;
@Service
@Validated
public class LocationResourceService {
private static final Logger log = LoggerFactory.getLogger(LocationResourceService.class);
@Resource
private LocationRepository repository;
@Resource
@@ -165,13 +170,8 @@ public class LocationResourceService {
if (!"OCCUPIED".equals(occupancy.getOccupancyStatus())) {
continue;
}
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId());
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
throw new BizException(ResultCode.CONFLICT, "释放占用时库位状态已被并发修改");
}
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
"释放占用时库位状态已被并发修改");
released.add(active(occupancy, "RELEASED", null));
}
for (LocationReservationEntity reservation : repository.findReservationsByBusiness(
@@ -237,15 +237,8 @@ public class LocationResourceService {
List<SelectiveReleaseResult> released = new ArrayList<>(selectedIndexes.size());
for (int index : selectedIndexes) {
LocationOccupancyEntity occupancy = active.get(index);
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(),
occupancy.getLocationId());
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
throw new BizException(ResultCode.CONFLICT,
"Location occupancy changed concurrently while releasing selected locations");
}
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
"Location occupancy changed concurrently while releasing selected locations");
released.add(new SelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(),
occupancy.getLocationCode(), packages.get(index)));
@@ -289,15 +282,8 @@ public class LocationResourceService {
throw new BizException(ResultCode.CONFLICT,
"Occupied location has no valid allocated package count and cannot be released exactly");
}
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(),
occupancy.getLocationId());
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
throw new BizException(ResultCode.CONFLICT,
"Location occupancy changed concurrently while releasing selected locations");
}
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
"Location occupancy changed concurrently while releasing selected locations");
released.add(new ExactSelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(),
occupancy.getLocationCode(), allocatedPackageCount));
@@ -322,19 +308,94 @@ public class LocationResourceService {
LocationOccupancyEntity occupancy = active.get(index);
if (!requested.contains(occupancy.getReservationId())) continue;
Integer allocated = occupancy.getAllocatedPackageCount();
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId());
if (allocated == null || allocated < 0 || location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow()) && repository.changeStatus(command.tenantId(),
occupancy.getWarehouseId(), occupancy.getLocationId(), "OCCUPIED", "AVAILABLE", location.getVersion(),
command.operatorId()) != 1)) {
if (allocated == null || allocated < 0) {
throw new BizException(ResultCode.CONFLICT, "释放库位资源时发生并发变更");
}
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(), "释放库位资源时发生并发变更");
result.add(new ExactSelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(), occupancy.getLocationCode(), allocated));
}
return List.copyOf(result);
}
/**
* Occupancy, confirmed reservation and the materialized location status form one resource fact.
* Lock the location before changing either fact so the final availability decision is serialized.
*/
private void releaseOccupiedResource(LocationOccupancyEntity occupancy, String reason, long operatorId,
String conflictMessage) {
LocationEntity location = repository.lockOwned(occupancy.getTenantId(), occupancy.getWarehouseId(),
occupancy.getLocationId());
LocationReservationEntity reservation = repository.lockReservation(
occupancy.getTenantId(), occupancy.getReservationId());
if (!matchesConfirmedResource(occupancy, reservation) || location == null) {
warnReleaseConflict(occupancy, location, reservation, 0, 0, -1, "RESOURCE_FACT_MISMATCH");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
int occupancyAffected = repository.releaseOccupancy(occupancy, reason);
if (occupancyAffected != 1) {
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, 0, -1,
"OCCUPANCY_UPDATE_MISSED");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
int reservationAffected = repository.releaseConfirmedReservation(reservation, reason);
if (reservationAffected != 1) {
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected, -1,
"RESERVATION_UPDATE_MISSED");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
if (Boolean.TRUE.equals(location.getSharedOverflow())) {
return;
}
int remainingActive = repository.countActiveOccupancies(occupancy.getTenantId(), occupancy.getLocationId());
if (remainingActive > 0) {
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected,
remainingActive, "NON_SHARED_LOCATION_STILL_OCCUPIED");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
if ("AVAILABLE".equals(location.getStatus())) {
log.warn("warehouse_location_release_converged reservationId={} occupancyId={} locationId={} "
+ "expectedLocationStatus=OCCUPIED actualLocationStatus={} occupancyAffected={} "
+ "reservationAffected={} remainingActiveOccupancies={}",
occupancy.getReservationId(), occupancy.getId(), occupancy.getLocationId(), location.getStatus(),
occupancyAffected, reservationAffected, remainingActive);
return;
}
if (!"OCCUPIED".equals(location.getStatus())
|| repository.changeStatus(occupancy.getTenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
"OCCUPIED", "AVAILABLE", location.getVersion(), operatorId) != 1) {
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected,
remainingActive, "LOCATION_STATUS_UPDATE_MISSED");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
}
private static boolean matchesConfirmedResource(LocationOccupancyEntity occupancy,
LocationReservationEntity reservation) {
return reservation != null
&& "CONFIRMED".equals(reservation.getReservationStatus())
&& Objects.equals(reservation.getWarehouseId(), occupancy.getWarehouseId())
&& Objects.equals(reservation.getBusinessId(), occupancy.getBusinessId())
&& Objects.equals(reservation.getLocationId(), occupancy.getLocationId())
&& Objects.equals(reservation.getPalletId(), occupancy.getPalletId())
&& Objects.equals(reservation.getSourceService(), occupancy.getSourceService());
}
private static void warnReleaseConflict(LocationOccupancyEntity occupancy, LocationEntity location,
LocationReservationEntity reservation, int occupancyAffected,
int reservationAffected, int remainingActive, String cause) {
log.warn("warehouse_location_release_conflict cause={} reservationId={} occupancyId={} locationId={} "
+ "expectedReservationStatus=CONFIRMED actualReservationStatus={} "
+ "expectedOccupancyStatus=OCCUPIED actualOccupancyStatus={} "
+ "expectedLocationStatus=OCCUPIED actualLocationStatus={} occupancyAffected={} "
+ "reservationAffected={} remainingActiveOccupancies={}",
cause, occupancy.getReservationId(), occupancy.getId(), occupancy.getLocationId(),
reservation == null ? null : reservation.getReservationStatus(), occupancy.getOccupancyStatus(),
location == null ? null : location.getStatus(), occupancyAffected, reservationAffected, remainingActive);
}
/**
* 将未拣货的已占用库位从主入库单转给二级子入库单。
* 转移只改变业务归属和剩余箱数,不释放库位;托盘仅作为关联快照。
@@ -85,9 +85,7 @@ ON DUPLICATE KEY UPDATE
location_type=VALUES(location_type),
max_weight_kg=VALUES(max_weight_kg),
shared_overflow=VALUES(shared_overflow),
status='AVAILABLE',
remark=VALUES(remark),
deleted=0;
remark=VALUES(remark);
INSERT INTO wh_location(
id,tenant_id,warehouse_id,zone_code,zone_name,location_code,location_name,
@@ -106,9 +104,7 @@ ON DUPLICATE KEY UPDATE
location_type=VALUES(location_type),
max_weight_kg=VALUES(max_weight_kg),
shared_overflow=VALUES(shared_overflow),
status='AVAILABLE',
remark=VALUES(remark),
deleted=0;
remark=VALUES(remark);
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
DELIMITER //