Files
baiyue-firstleg/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceService.java
T
2026-09-24 08:54:26 +08:00

468 lines
25 KiB
Java

package com.baiyue.warehouse.service;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.LocationRequests;
import com.baiyue.warehouse.dto.PalletRequests;
import com.baiyue.warehouse.dto.WarehousePalletResolveResponse;
import com.baiyue.warehouse.dto.WarehouseLocationResolveResponse;
import com.baiyue.warehouse.dto.SharedWarehouseReference;
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
import com.baiyue.warehouse.entity.WarehousePalletEntity;
import com.baiyue.warehouse.entity.WarehousePalletBindingEntity;
import com.baiyue.warehouse.repository.WarehouseResourceRepository;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.util.Set;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import org.springframework.util.StringUtils;
@Service
public class WarehouseResourceService {
public static final long PUBLIC_RESOURCE_SCOPE = 0L;
public static final String SHARED_OVERFLOW_LOCATION_CODE = "A0";
private static final Set<String> LOCATION_STATUSES = Set.of("ACTIVE", "IDLE", "OCCUPIED", "INACTIVE");
private static final Set<String> PALLET_STATUSES = Set.of("IDLE", "OCCUPIED", "SCRAPPED");
@Resource private WarehouseResourceRepository repository;
@Resource private DomesticWarehouseDirectory warehouseDirectory;
@Transactional
public Long createLocation(long customerId, long warehouseId, LocationRequests.SaveRequest request) {
requireWarehouse(customerId, warehouseId);
validateLocation(request, customerId, warehouseId, null);
WarehouseLocationEntity entity = locationEntity(customerId, warehouseId, request);
repository.insertLocation(entity);
return entity.getId();
}
@Transactional
public void updateLocation(long customerId, long warehouseId, long id, LocationRequests.SaveRequest request) {
WarehouseLocationEntity existing = requireLocation(customerId, warehouseId, id);
if (isSharedOverflow(existing)) {
throw new BizException(ResultCode.CONFLICT, "系统共享溢出库位 A0 不允许编辑");
}
if ("VIRTUAL".equals(existing.getLocationType())
&& (!"VIRTUAL".equals(request.locationType()) || !"ACTIVE".equals(request.status())
|| !existing.getLocationCode().equals(request.locationCode()))) {
throw new BizException(ResultCode.CONFLICT, "Default virtual location must remain VIRTUAL and ACTIVE");
}
validateLocation(request, customerId, warehouseId, id);
WarehouseLocationEntity entity = locationEntity(customerId, warehouseId, request);
entity.setId(existing.getId());
repository.updateLocation(entity);
}
public WarehouseLocationEntity location(long customerId, long warehouseId, long id) {
requireWarehouse(customerId, warehouseId);
return requireLocation(customerId, warehouseId, id);
}
public PageResult<WarehouseLocationEntity> locations(long customerId, long warehouseId,
LocationRequests.QueryRequest query) {
requireWarehouse(customerId, warehouseId);
if (customerId == PUBLIC_RESOURCE_SCOPE) ensureSharedOverflowLocation(warehouseId);
return repository.pageLocations(customerId, warehouseId, query);
}
@Transactional
public void deleteLocation(long customerId, long warehouseId, long id) {
WarehouseLocationEntity location = requireLocation(customerId, warehouseId, id);
if (isSharedOverflow(location)) {
throw new BizException(ResultCode.CONFLICT, "系统共享溢出库位 A0 不允许删除");
}
if ("VIRTUAL".equals(location.getLocationType())) {
throw new BizException(ResultCode.CONFLICT, "Default virtual location cannot be deleted");
}
if (repository.locationHasPallets(customerId, warehouseId, id)) {
throw new BizException(ResultCode.CONFLICT, "库位仍绑定托盘,不能删除");
}
repository.deleteLocation(id);
}
@Transactional
public Long createPallet(long customerId, long warehouseId, PalletRequests.SaveRequest request) {
SharedWarehouseReference warehouse = requireWarehouseReference(warehouseId);
validatePalletStatus(request.status());
Long palletId = IdWorker.getId();
String palletCode = StringUtils.hasText(request.palletCode()) ? request.palletCode().trim()
: generatedPalletCode(warehouse.warehouseCode(), palletId);
Long locationId = request.locationId();
if (locationId == null) {
locationId = ensureDefaultVirtualLocation(customerId, warehouseId).getId();
}
validatePallet(request, palletCode, locationId, customerId, warehouseId, null);
WarehousePalletEntity entity = palletEntity(customerId, warehouseId, request, palletCode, locationId);
entity.setId(palletId);
repository.insertPallet(entity);
return entity.getId();
}
@Transactional
public WarehousePalletEntity ensureDropshipPallet(long customerId, long warehouseId, String businessId) {
WarehousePalletBindingEntity existing = repository.findPalletBinding(customerId, warehouseId, "DROPSHIP", businessId);
if (existing != null) return requirePallet(customerId, warehouseId, existing.getPalletId());
Long palletId = createPallet(customerId, warehouseId,
new PalletRequests.SaveRequest(null, "DROPSHIP", null, java.math.BigDecimal.ZERO, "IDLE", null,
"Default dropship pallet for " + businessId));
WarehousePalletBindingEntity binding = new WarehousePalletBindingEntity();
binding.setId(IdWorker.getId()); binding.setCustomerId(customerId); binding.setWarehouseId(warehouseId);
binding.setBusinessType("DROPSHIP"); binding.setBusinessId(businessId); binding.setPalletId(palletId);
binding.setCreatedAt(java.time.LocalDateTime.now());
try {
repository.insertPalletBinding(binding);
return requirePallet(customerId, warehouseId, palletId);
} catch (org.springframework.dao.DuplicateKeyException duplicate) {
WarehousePalletBindingEntity winner = repository.findPalletBinding(customerId, warehouseId, "DROPSHIP", businessId);
if (winner == null) throw duplicate;
return requirePallet(customerId, warehouseId, winner.getPalletId());
}
}
@Transactional
public WarehousePalletResolveResponse ensureRelabelPallet(long warehouseId, String businessId) {
return ensureRelabelPallet(warehouseId, businessId, null);
}
@Transactional
public WarehousePalletResolveResponse ensureRelabelPallet(long warehouseId, String businessId,
String fbaWarehouseCode) {
String normalizedBusinessId = businessId == null ? null : businessId.trim();
String normalizedFbaWarehouseCode = StringUtils.hasText(fbaWarehouseCode) ? fbaWarehouseCode.trim() : null;
WarehousePalletBindingEntity existing = repository.findPalletBinding(PUBLIC_RESOURCE_SCOPE, warehouseId,
"RELABEL_PDA_NEW", normalizedBusinessId);
if (existing != null) return requireRelabelPalletDestination(existing.getPalletId(), normalizedFbaWarehouseCode);
Long palletId = createPallet(PUBLIC_RESOURCE_SCOPE, warehouseId,
new PalletRequests.SaveRequest(null, "STANDARD", null, java.math.BigDecimal.ZERO, "IDLE", null,
"PDA换标拆托新托盘", normalizedFbaWarehouseCode));
WarehousePalletBindingEntity binding = new WarehousePalletBindingEntity();
binding.setId(IdWorker.getId()); binding.setCustomerId(PUBLIC_RESOURCE_SCOPE); binding.setWarehouseId(warehouseId);
binding.setBusinessType("RELABEL_PDA_NEW"); binding.setBusinessId(normalizedBusinessId); binding.setPalletId(palletId);
binding.setCreatedAt(java.time.LocalDateTime.now());
try {
repository.insertPalletBinding(binding);
return resolvePublicPallet(palletId);
} catch (org.springframework.dao.DuplicateKeyException duplicate) {
WarehousePalletBindingEntity winner = repository.findPalletBinding(PUBLIC_RESOURCE_SCOPE, warehouseId,
"RELABEL_PDA_NEW", normalizedBusinessId);
if (winner == null) throw duplicate;
return requireRelabelPalletDestination(winner.getPalletId(), normalizedFbaWarehouseCode);
}
}
private WarehousePalletResolveResponse requireRelabelPalletDestination(long palletId, String fbaWarehouseCode) {
WarehousePalletResolveResponse pallet = resolvePublicPallet(palletId);
if (fbaWarehouseCode != null && !fbaWarehouseCode.equalsIgnoreCase(
java.util.Objects.toString(pallet.fbaWarehouseCode(), ""))) {
throw new BizException(ResultCode.CONFLICT, "换标新托盘的 FBA 目的仓与当前申请不一致");
}
return pallet;
}
/**
* Returns the warehouse-scoped virtual location used only as a pallet/inventory reference.
* It is deliberately never made IDLE/OCCUPIED by resource locking.
*/
@Transactional
public WarehouseLocationEntity ensureDefaultVirtualLocation(long customerId, long warehouseId) {
SharedWarehouseReference warehouse = requireWarehouseReference(warehouseId);
String locationCode = defaultLocationCode(warehouse.warehouseCode());
WarehouseLocationEntity existing = repository.findLocationByCode(customerId, warehouseId, locationCode);
if (existing != null) {
if (!"VIRTUAL".equals(existing.getLocationType()) || !"ACTIVE".equals(existing.getStatus())) {
throw new BizException(ResultCode.CONFLICT,
"Default virtual location code is occupied by a non-active virtual location");
}
return existing;
}
WarehouseLocationEntity location = new WarehouseLocationEntity();
location.setId(IdWorker.getId());
location.setCustomerId(customerId);
location.setWarehouseId(warehouseId);
location.setLocationCode(locationCode);
location.setLocationType("VIRTUAL");
location.setMaxLoadKg(new java.math.BigDecimal("999999999.000"));
location.setStatus("ACTIVE");
location.setRemark("System default virtual location");
repository.insertDefaultVirtualLocationIgnore(location);
WarehouseLocationEntity resolved = repository.findLocationByCode(customerId, warehouseId, locationCode);
if (resolved == null) {
throw new BizException(ResultCode.CONFLICT, "Default virtual location could not be created");
}
return resolved;
}
@Transactional
public WarehouseLocationEntity ensureSharedOverflowLocation(long warehouseId) {
SharedWarehouseReference warehouse = requireWarehouseReference(warehouseId);
WarehouseLocationEntity existing = repository.findSharedOverflowLocation(warehouseId);
if (existing != null) {
if (!isSharedOverflow(existing)) {
throw new BizException(ResultCode.CONFLICT, "A0库位编码已被非共享溢出库位占用");
}
return existing;
}
WarehouseLocationEntity location = new WarehouseLocationEntity();
location.setId(IdWorker.getId());
location.setCustomerId(PUBLIC_RESOURCE_SCOPE);
location.setWarehouseId(warehouseId);
location.setLocationCode(SHARED_OVERFLOW_LOCATION_CODE);
location.setLocationType("SHARED_OVERFLOW");
location.setSharedOverflow(true);
location.setMaxLoadKg(new java.math.BigDecimal("999999999.000"));
location.setStatus("ACTIVE");
location.setRemark("系统共享溢出库位,不得停用或删除");
try {
repository.insertLocation(location);
} catch (org.springframework.dao.DuplicateKeyException ignored) {
// Concurrent first use: read the row created by the winner.
}
WarehouseLocationEntity resolved = repository.findLocationByCode(PUBLIC_RESOURCE_SCOPE, warehouseId,
SHARED_OVERFLOW_LOCATION_CODE);
if (resolved == null || !isSharedOverflow(resolved)) {
throw new BizException(ResultCode.CONFLICT, "共享溢出库位 A0 创建失败");
}
return resolved;
}
@Transactional
public void updatePallet(long customerId, long warehouseId, long id, PalletRequests.SaveRequest request) {
WarehousePalletEntity existing = requirePallet(customerId, warehouseId, id);
if (request.version() == null || !request.version().equals(existing.getVersion())) {
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请刷新后重试");
}
if ("SCRAPPED".equals(existing.getStatus()) && !"SCRAPPED".equals(request.status())) {
throw new BizException(ResultCode.CONFLICT, "已报废的托盘不允许修改为其他状态");
}
String palletCode = StringUtils.hasText(request.palletCode()) ? request.palletCode().trim()
: existing.getPalletCode();
Long locationId = request.locationId() == null ? existing.getLocationId() : request.locationId();
validatePallet(request, palletCode, locationId, customerId, warehouseId, id);
WarehousePalletEntity entity = palletEntity(customerId, warehouseId, request, palletCode, locationId);
entity.setId(existing.getId());
entity.setVersion(request.version());
if (repository.updatePallet(entity) != 1) {
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请刷新后重试");
}
}
public WarehousePalletEntity pallet(long customerId, long warehouseId, long id) {
requireWarehouse(customerId, warehouseId);
return requirePallet(customerId, warehouseId, id);
}
public WarehousePalletResolveResponse resolvePublicPallet(String palletCode) {
if (!StringUtils.hasText(palletCode)) {
throw new BizException(ResultCode.BAD_REQUEST, "托盘码不能为空");
}
var matches = repository.findPublicPalletsByCode(palletCode.trim());
if (matches.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "托盘码不存在");
if (matches.size() != 1) throw new BizException(ResultCode.CONFLICT, "托盘码在多个仓库重复,无法解析");
WarehousePalletEntity pallet = matches.get(0);
if ("SCRAPPED".equals(pallet.getStatus())) throw new BizException(ResultCode.CONFLICT, "托盘已报废");
return palletReference(pallet);
}
public WarehousePalletEntity reusablePublicPallet(long warehouseId, String palletCode) {
if (!StringUtils.hasText(palletCode)) return null;
requireWarehouseReference(warehouseId);
return repository.findPublicPalletByCode(warehouseId, palletCode.trim());
}
public WarehousePalletResolveResponse resolvePublicPallet(long palletId) {
WarehousePalletEntity pallet = repository.findPublicPallet(palletId);
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在");
return palletReference(pallet);
}
public WarehouseLocationResolveResponse resolvePublicLocation(long warehouseId, String locationCode) {
if (!StringUtils.hasText(locationCode)) throw new BizException(ResultCode.BAD_REQUEST, "库位码不能为空");
if (SHARED_OVERFLOW_LOCATION_CODE.equalsIgnoreCase(locationCode.trim())) {
return locationReference(ensureSharedOverflowLocation(warehouseId));
}
WarehouseLocationEntity location = repository.findLocationByCode(PUBLIC_RESOURCE_SCOPE, warehouseId,
locationCode.trim());
if (location == null) throw new BizException(ResultCode.NOT_FOUND, "库位码不存在或不属于当前仓库");
return locationReference(location);
}
public WarehouseLocationResolveResponse resolvePublicLocation(long warehouseId, long locationId) {
WarehouseLocationEntity location = repository.findLocation(PUBLIC_RESOURCE_SCOPE, warehouseId, locationId);
if (location == null) throw new BizException(ResultCode.NOT_FOUND, "库位不存在或不属于当前仓库");
return locationReference(location);
}
private static WarehouseLocationResolveResponse locationReference(WarehouseLocationEntity location) {
return new WarehouseLocationResolveResponse(location.getId(), location.getLocationCode(),
location.getLocationType(), isSharedOverflow(location), location.getMaxLoadKg(), location.getStatus());
}
private WarehousePalletResolveResponse palletReference(WarehousePalletEntity pallet) {
WarehouseLocationEntity location = pallet.getLocationId() == null ? null
: repository.findLocation(PUBLIC_RESOURCE_SCOPE, pallet.getWarehouseId(), pallet.getLocationId());
String blockReason = reservationBlockReason(pallet, location);
return new WarehousePalletResolveResponse(pallet.getId(), pallet.getPalletCode(), pallet.getWarehouseId(),
pallet.getLocationId(), pallet.getStatus(), pallet.getFbaWarehouseCode(), pallet.getUnitPrice(),
pallet.getVersion() == null ? 0 : pallet.getVersion(),
location == null ? null : location.getLocationType(),
location == null ? null : location.getStatus(), blockReason == null, blockReason);
}
private static String reservationBlockReason(WarehousePalletEntity pallet, WarehouseLocationEntity location) {
if (!"IDLE".equals(pallet.getStatus())) return "PALLET_NOT_IDLE";
if (location == null) return pallet.getLocationId() == null ? "LOCATION_NOT_REQUIRED" : "LOCATION_NOT_FOUND";
if ("VIRTUAL".equals(location.getLocationType())) {
return "ACTIVE".equals(location.getStatus()) ? null : "VIRTUAL_LOCATION_NOT_ACTIVE";
}
return "IDLE".equals(location.getStatus()) ? null : "LOCATION_NOT_IDLE";
}
public PageResult<WarehousePalletEntity> pallets(long customerId, long warehouseId,
PalletRequests.QueryRequest query) {
requireWarehouse(customerId, warehouseId);
return repository.pagePallets(customerId, warehouseId, query);
}
public PalletRequests.Summary palletSummary(long customerId, long warehouseId, PalletRequests.QueryRequest query) {
requireWarehouse(customerId, warehouseId);
long total = repository.countPallets(customerId, warehouseId, query, null);
return new PalletRequests.Summary(total,
repository.countPallets(customerId, warehouseId, query, "IDLE"),
repository.countPallets(customerId, warehouseId, query, "OCCUPIED"),
repository.countPallets(customerId, warehouseId, query, "SCRAPPED"));
}
public PalletRequests.Summary publicPalletSummary(long customerId, PalletRequests.QueryRequest query) {
long total = repository.countPallets(customerId, (Long) null, query, null);
return new PalletRequests.Summary(total,
repository.countPallets(customerId, (Long) null, query, "IDLE"),
repository.countPallets(customerId, (Long) null, query, "OCCUPIED"),
repository.countPallets(customerId, (Long) null, query, "SCRAPPED"));
}
public PageResult<WarehousePalletEntity> publicPallets(long customerId,
PalletRequests.QueryRequest query) {
return repository.pagePublicPallets(customerId, query);
}
@Transactional
public void deletePallet(long customerId, long warehouseId, long id) {
WarehousePalletEntity pallet = requirePallet(customerId, warehouseId, id);
if ("OCCUPIED".equals(pallet.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "占用中的托盘不能删除");
}
if ("SCRAPPED".equals(pallet.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "Scrapped pallet must be retained and cannot be reused");
}
repository.deletePallet(id);
}
private void requireWarehouse(long customerId, long warehouseId) {
warehouseDirectory.required(warehouseId);
}
private SharedWarehouseReference requireWarehouseReference(long warehouseId) {
return warehouseDirectory.required(warehouseId);
}
private WarehouseLocationEntity requireLocation(long customerId, long warehouseId, long id) {
requireWarehouse(customerId, warehouseId);
WarehouseLocationEntity entity = repository.findLocation(customerId, warehouseId, id);
if (entity == null) throw new BizException(ResultCode.NOT_FOUND, "当前仓库下库位不存在");
return entity;
}
private WarehousePalletEntity requirePallet(long customerId, long warehouseId, long id) {
requireWarehouse(customerId, warehouseId);
WarehousePalletEntity entity = repository.findPallet(customerId, warehouseId, id);
if (entity == null) throw new BizException(ResultCode.NOT_FOUND, "当前仓库下托盘不存在");
return entity;
}
private void validateLocation(LocationRequests.SaveRequest request, long customerId,
long warehouseId, Long excludedId) {
if (SHARED_OVERFLOW_LOCATION_CODE.equalsIgnoreCase(request.locationCode())
|| "SHARED_OVERFLOW".equals(request.locationType())) {
throw new BizException(ResultCode.CONFLICT, "A0 是系统共享溢出库位,只能由系统按仓库懒创建");
}
if (!LOCATION_STATUSES.contains(request.status())) {
throw new BizException("库位状态仅支持 ACTIVE、IDLE、OCCUPIED、INACTIVE");
}
if (repository.locationCodeExists(customerId, warehouseId, request.locationCode(), excludedId)) {
throw new BizException(ResultCode.CONFLICT, "当前仓库下库位编码已存在");
}
}
private void validatePallet(PalletRequests.SaveRequest request, String palletCode, Long locationId,
long customerId, long warehouseId, Long excludedId) {
validatePalletStatus(request.status());
if (repository.palletCodeExists(customerId, warehouseId, palletCode, excludedId)) {
throw new BizException(ResultCode.CONFLICT, "当前仓库下托盘编码已存在");
}
if (locationId != null) {
requireLocation(customerId, warehouseId, locationId);
}
}
private void validatePalletStatus(String status) {
if (!PALLET_STATUSES.contains(status)) {
throw new BizException("托盘状态仅支持 IDLE、OCCUPIED、SCRAPPED");
}
}
private WarehouseLocationEntity locationEntity(long customerId, long warehouseId,
LocationRequests.SaveRequest request) {
WarehouseLocationEntity entity = new WarehouseLocationEntity();
entity.setCustomerId(customerId);
entity.setWarehouseId(warehouseId);
entity.setLocationCode(request.locationCode());
entity.setLocationType(request.locationType());
entity.setMaxLoadKg(request.maxLoadKg());
entity.setStatus(request.status());
entity.setRemark(request.remark());
return entity;
}
private WarehousePalletEntity palletEntity(long customerId, long warehouseId,
PalletRequests.SaveRequest request,
String palletCode, Long locationId) {
WarehousePalletEntity entity = new WarehousePalletEntity();
entity.setCustomerId(customerId);
entity.setWarehouseId(warehouseId);
entity.setLocationId(locationId);
entity.setPalletCode(palletCode);
entity.setPalletType(request.palletType());
entity.setCapacityCbm(request.capacityCbm());
entity.setUnitPrice(request.unitPrice());
entity.setStatus(request.status());
entity.setRemark(request.remark());
entity.setFbaWarehouseCode(request.fbaWarehouseCode());
return entity;
}
private static String generatedPalletCode(String warehouseCode, long palletId) {
String compactWarehouseCode = warehouseCode.replaceAll("[^A-Za-z0-9]", "").toUpperCase(java.util.Locale.ROOT);
if (compactWarehouseCode.length() > 9) compactWarehouseCode = compactWarehouseCode.substring(0, 9);
return "PLT-" + compactWarehouseCode + "-"
+ LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd")) + "-"
+ Long.toUnsignedString(palletId, 36).toUpperCase(java.util.Locale.ROOT);
}
private static String defaultLocationCode(String warehouseCode) {
return "LOC-" + warehouseCode + "-DEFAULT";
}
public static boolean isSharedOverflow(WarehouseLocationEntity location) {
return location != null && Boolean.TRUE.equals(location.getSharedOverflow())
&& "SHARED_OVERFLOW".equals(location.getLocationType())
&& SHARED_OVERFLOW_LOCATION_CODE.equalsIgnoreCase(location.getLocationCode());
}
}