2026-08-19 16:24:02 +08:00
|
|
|
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.entity.WarehouseLocationEntity;
|
|
|
|
|
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
|
|
|
|
import com.baiyue.warehouse.entity.WarehouseEntity;
|
|
|
|
|
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 {
|
2026-08-20 18:02:38 +08:00
|
|
|
public static final long PUBLIC_RESOURCE_SCOPE = 0L;
|
2026-08-19 16:24:02 +08:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
@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 ("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);
|
|
|
|
|
return repository.pageLocations(customerId, warehouseId, query);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Transactional
|
|
|
|
|
public void deleteLocation(long customerId, long warehouseId, long id) {
|
|
|
|
|
WarehouseLocationEntity location = requireLocation(customerId, warehouseId, id);
|
|
|
|
|
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) {
|
|
|
|
|
WarehouseEntity warehouse = requireWarehouseEntity(customerId, warehouseId);
|
|
|
|
|
validatePalletStatus(request.status());
|
|
|
|
|
Long palletId = IdWorker.getId();
|
|
|
|
|
String palletCode = StringUtils.hasText(request.palletCode()) ? request.palletCode().trim()
|
|
|
|
|
: generatedPalletCode(warehouse.getWarehouseCode(), 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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
|
|
|
|
WarehouseEntity warehouse = requireWarehouseEntity(customerId, warehouseId);
|
|
|
|
|
String locationCode = defaultLocationCode(warehouse.getWarehouseCode());
|
|
|
|
|
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.setZoneCode("DEFAULT");
|
|
|
|
|
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 void updatePallet(long customerId, long warehouseId, long id, PalletRequests.SaveRequest request) {
|
|
|
|
|
WarehousePalletEntity existing = requirePallet(customerId, warehouseId, id);
|
2026-08-24 15:14:18 +08:00
|
|
|
if (request.version() == null || !request.version().equals(existing.getVersion())) {
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请刷新后重试");
|
|
|
|
|
}
|
2026-08-19 16:24:02 +08:00
|
|
|
if ("SCRAPPED".equals(existing.getStatus()) && !"SCRAPPED".equals(request.status())) {
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "SCRAPPED pallet cannot be reused");
|
|
|
|
|
}
|
|
|
|
|
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());
|
2026-08-24 15:14:18 +08:00
|
|
|
entity.setVersion(request.version());
|
|
|
|
|
if (repository.updatePallet(entity) != 1) {
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请刷新后重试");
|
|
|
|
|
}
|
2026-08-19 16:24:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public WarehousePalletEntity pallet(long customerId, long warehouseId, long id) {
|
|
|
|
|
requireWarehouse(customerId, warehouseId);
|
|
|
|
|
return requirePallet(customerId, warehouseId, id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PageResult<WarehousePalletEntity> pallets(long customerId, long warehouseId,
|
|
|
|
|
PalletRequests.QueryRequest query) {
|
|
|
|
|
requireWarehouse(customerId, warehouseId);
|
|
|
|
|
return repository.pagePallets(customerId, warehouseId, 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) {
|
2026-08-20 18:02:38 +08:00
|
|
|
if (customerId == PUBLIC_RESOURCE_SCOPE) {
|
|
|
|
|
if (repository.findWarehouseById(warehouseId) == null) {
|
|
|
|
|
throw new BizException(ResultCode.NOT_FOUND, "Warehouse not found");
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-08-19 16:24:02 +08:00
|
|
|
if (!repository.warehouseOwned(customerId, warehouseId)) {
|
|
|
|
|
throw new BizException(ResultCode.NOT_FOUND, "当前租户下仓库不存在");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private WarehouseEntity requireWarehouseEntity(long customerId, long warehouseId) {
|
2026-08-20 18:02:38 +08:00
|
|
|
WarehouseEntity warehouse = customerId == PUBLIC_RESOURCE_SCOPE
|
|
|
|
|
? repository.findWarehouseById(warehouseId) : repository.findWarehouse(customerId, warehouseId);
|
2026-08-19 16:24:02 +08:00
|
|
|
if (warehouse == null) {
|
|
|
|
|
throw new BizException(ResultCode.NOT_FOUND, "Tenant warehouse not found");
|
|
|
|
|
}
|
|
|
|
|
return warehouse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 (!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.setZoneCode(request.zoneCode());
|
|
|
|
|
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());
|
2026-08-21 11:43:03 +08:00
|
|
|
entity.setFbaWarehouseCode(request.fbaWarehouseCode());
|
2026-08-19 16:24:02 +08:00
|
|
|
return entity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String generatedPalletCode(String warehouseCode, long palletId) {
|
|
|
|
|
return "PLT-" + warehouseCode + "-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
|
|
|
|
|
+ "-" + Long.toUnsignedString(palletId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String defaultLocationCode(String warehouseCode) {
|
|
|
|
|
return "LOC-" + warehouseCode + "-DEFAULT";
|
|
|
|
|
}
|
|
|
|
|
}
|