feat: complete finance and overseas receiving workflow

This commit is contained in:
2026-09-10 22:43:11 +08:00
parent e531ae9407
commit 56635ad4e4
66 changed files with 1601 additions and 417 deletions
@@ -21,11 +21,18 @@ public interface WarehouseResourceLockMapper extends BaseMapper<WarehouseResourc
@Select("SELECT * FROM wh_resource_lock WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE")
WarehouseResourceLockEntity lockById(@Param("customerId") long customerId, @Param("id") long id);
@Select("SELECT * FROM wh_resource_lock WHERE id=#{id} LIMIT 1 FOR UPDATE")
WarehouseResourceLockEntity lockAnyById(@Param("id") long id);
@Select("SELECT * FROM wh_resource_lock WHERE customer_id=#{customerId} AND pallet_id=#{palletId} " +
"AND status='ACTIVE' ORDER BY locked_at DESC LIMIT 1")
WarehouseResourceLockEntity findActiveByPallet(@Param("customerId") long customerId,
@Param("palletId") long palletId);
@Select("SELECT * FROM wh_resource_lock WHERE pallet_id=#{palletId} AND status='ACTIVE' " +
"ORDER BY locked_at DESC LIMIT 1")
WarehouseResourceLockEntity findAnyActiveByPallet(@Param("palletId") long palletId);
@Update("""
UPDATE wh_pallet
SET status = 'OCCUPIED', location_id = #{locationId}, updated_at = CURRENT_TIMESTAMP(3), version = version + 1
@@ -26,10 +26,16 @@ public class WarehouseResourceLockRepository {
return locks.lockById(customerId, lockId);
}
public WarehouseResourceLockEntity findAny(long lockId) { return locks.lockAnyById(lockId); }
public WarehouseResourceLockEntity findActiveByPallet(long customerId, long palletId) {
return locks.findActiveByPallet(customerId, palletId);
}
public WarehouseResourceLockEntity findAnyActiveByPallet(long palletId) {
return locks.findAnyActiveByPallet(palletId);
}
public WarehousePalletEntity findPallet(long customerId, long palletId) {
return pallets.selectOne(new LambdaQueryWrapper<WarehousePalletEntity>()
.eq(WarehousePalletEntity::getCustomerId, PUBLIC_RESOURCE_SCOPE)
@@ -32,6 +32,8 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -81,16 +83,24 @@ public class WarehousePalletBatchService {
}
List<WarehousePalletBatchItemEntity> created = new ArrayList<>();
Map<String, SharedReservation> sharedDropship = new LinkedHashMap<>();
for (WarehousePalletBatchRequests.Item requested : normalized.items()) {
Long palletId = resources.createPallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
normalized.warehouseId(), new PalletRequests.SaveRequest(requested.palletCode(), "WOOD",
AUTO_PALLET_CAPACITY_CBM, AUTO_PALLET_UNIT_PRICE_USD, "IDLE", null, AUTO_PALLET_REMARK,
requested.fbaWarehouseCode()));
WarehousePalletEntity pallet = resources.pallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
normalized.warehouseId(), palletId);
WarehouseResourceLockResponse lock = locks.reserve(new WarehouseResourceLockRequests.ReserveRequest(
requested.customerId(), normalized.warehouseId(), pallet.getId(), pallet.getLocationId(),
BUSINESS_TYPE, requested.businessKey(), reservationKey(batch, requested.sequenceNo())));
SharedReservation shared = isDropshipPallet(requested.palletCode())
? sharedDropship.get(requested.palletCode()) : null;
if (shared == null) {
Long palletId = resources.createPallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
normalized.warehouseId(), new PalletRequests.SaveRequest(requested.palletCode(), "WOOD",
AUTO_PALLET_CAPACITY_CBM, AUTO_PALLET_UNIT_PRICE_USD, "IDLE", null, AUTO_PALLET_REMARK,
requested.fbaWarehouseCode()));
WarehousePalletEntity createdPallet = resources.pallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
normalized.warehouseId(), palletId);
WarehouseResourceLockResponse lock = locks.reserve(new WarehouseResourceLockRequests.ReserveRequest(
requested.customerId(), normalized.warehouseId(), createdPallet.getId(), createdPallet.getLocationId(),
BUSINESS_TYPE, requested.businessKey(), reservationKey(batch, requested.sequenceNo())));
shared = new SharedReservation(createdPallet, lock.lockId());
if (isDropshipPallet(requested.palletCode())) sharedDropship.put(requested.palletCode(), shared);
}
WarehousePalletEntity pallet = shared.pallet();
WarehousePalletBatchItemEntity item = new WarehousePalletBatchItemEntity();
item.setId(IdWorker.getId());
@@ -103,7 +113,7 @@ public class WarehousePalletBatchService {
item.setPalletId(pallet.getId());
item.setPalletCode(pallet.getPalletCode());
item.setLocationId(pallet.getLocationId());
item.setResourceLockId(lock.lockId());
item.setResourceLockId(shared.lockId());
item.setStatus("RESERVED");
item.setCreatedAt(LocalDateTime.now());
item.setUpdatedAt(item.getCreatedAt());
@@ -127,13 +137,14 @@ public class WarehousePalletBatchService {
throw conflict("Only a RESERVED pallet batch can be confirmed");
}
for (WarehousePalletBatchItemEntity item : batchItems) {
var lock = lockRecords.find(item.getCustomerId(), item.getResourceLockId());
var lock = isDropshipPallet(item.getPalletCode())
? lockRecords.findAny(item.getResourceLockId())
: lockRecords.find(item.getCustomerId(), item.getResourceLockId());
if (lock == null || !"ACTIVE".equals(lock.getStatus())
|| !Objects.equals(lock.getWarehouseId(), batch.getWarehouseId())
|| !Objects.equals(lock.getPalletId(), item.getPalletId())
|| !Objects.equals(lock.getLocationId(), item.getLocationId())
|| !BUSINESS_TYPE.equals(lock.getBusinessType())
|| !item.getBusinessKey().equals(lock.getBusinessId())) {
|| !BUSINESS_TYPE.equals(lock.getBusinessType())) {
throw conflict("Inbound plan pallet reservation is no longer active");
}
WarehousePalletBindingEntity binding = repository.lockPalletBinding(item.getCustomerId(),
@@ -177,13 +188,22 @@ public class WarehousePalletBatchService {
if (!"RESERVED".equals(batch.getStatus())) {
throw conflict("Confirmed or subsequently occupied pallets cannot be compensated");
}
Set<Long> releasedLocks = new HashSet<>();
for (WarehousePalletBatchItemEntity item : batchItems) {
WarehousePalletBindingEntity binding = repository.lockPalletBinding(item.getCustomerId(),
batch.getWarehouseId(), BUSINESS_TYPE, item.getBusinessKey());
if (binding != null && Objects.equals(binding.getPalletId(), item.getPalletId())) {
throw conflict("A bound inbound plan pallet cannot be compensated");
}
locks.release(item.getCustomerId(), item.getResourceLockId());
if (releasedLocks.add(item.getResourceLockId())) {
long lockCustomerId = item.getCustomerId();
if (isDropshipPallet(item.getPalletCode())) {
var lock = lockRecords.findAny(item.getResourceLockId());
if (lock == null) throw conflict("Inbound plan pallet resource lock was not found");
lockCustomerId = lock.getCustomerId();
}
locks.release(lockCustomerId, item.getResourceLockId());
}
item.setStatus("RELEASED");
item.setUpdatedAt(LocalDateTime.now());
items.updateById(item);
@@ -208,12 +228,17 @@ public class WarehousePalletBatchService {
throw conflict("Inbound plan pallet business key became bound after compensation");
}
}
Map<Long, WarehouseResourceLockResponse> reactivated = new LinkedHashMap<>();
for (WarehousePalletBatchItemEntity item : batchItems) {
WarehousePalletEntity pallet = resources.pallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
batch.getWarehouseId(), item.getPalletId());
WarehouseResourceLockResponse lock = locks.reserve(new WarehouseResourceLockRequests.ReserveRequest(
item.getCustomerId(), batch.getWarehouseId(), pallet.getId(), item.getLocationId(), BUSINESS_TYPE,
item.getBusinessKey(), reservationKey(batch.getId(), attempt, item.getSequenceNo())));
WarehouseResourceLockResponse lock = reactivated.get(pallet.getId());
if (lock == null) {
lock = locks.reserve(new WarehouseResourceLockRequests.ReserveRequest(
item.getCustomerId(), batch.getWarehouseId(), pallet.getId(), item.getLocationId(), BUSINESS_TYPE,
item.getBusinessKey(), reservationKey(batch.getId(), attempt, item.getSequenceNo())));
reactivated.put(pallet.getId(), lock);
}
item.setResourceLockId(lock.lockId());
item.setStatus("RESERVED");
item.setUpdatedAt(LocalDateTime.now());
@@ -341,6 +366,10 @@ public class WarehousePalletBatchService {
private static int number(Integer value) { return value == null ? 0 : value; }
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
private static boolean isDropshipPallet(String palletCode) {
return palletCode != null && palletCode.trim().toUpperCase(java.util.Locale.ROOT).startsWith("DP");
}
private record SharedReservation(WarehousePalletEntity pallet, long lockId) { }
private static String required(String value, String message) {
String normalized = trim(value);
if (normalized == null) throw badRequest(message);
@@ -106,8 +106,12 @@ public class WarehouseResourceLockService {
throw new BizException(ResultCode.CONFLICT, "Only an OCCUPIED pallet can be scrapped after shipment");
}
WarehouseResourceLockEntity lock = repository.findActiveByPallet(customerId, palletId);
if (lock == null && pallet.getPalletCode() != null
&& pallet.getPalletCode().toUpperCase(java.util.Locale.ROOT).startsWith("DP-")) {
lock = repository.findAnyActiveByPallet(palletId);
}
if (lock != null) {
scrap(customerId, lock.getId());
scrap(lock.getCustomerId(), lock.getId());
} else if (repository.scrapPalletWithoutLock(customerId, palletId) != 1) {
throw new BizException(ResultCode.CONFLICT, "Pallet state changed concurrently");
}