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; private static final Set LOCATION_STATUSES = Set.of("ACTIVE", "IDLE", "OCCUPIED", "INACTIVE"); private static final Set 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 ("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 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) { 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()); } } /** * 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.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); 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, "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()); 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 (pallet.getLocationId() == null) throw new BizException(ResultCode.CONFLICT, "托盘未绑定库位"); if ("SCRAPPED".equals(pallet.getStatus())) throw new BizException(ResultCode.CONFLICT, "托盘已报废"); return palletReference(pallet); } public WarehousePalletResolveResponse resolvePublicPallet(long palletId) { WarehousePalletEntity pallet = repository.findPublicPallet(palletId); if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在"); if (pallet.getLocationId() == null) throw new BizException(ResultCode.CONFLICT, "托盘未绑定库位"); return palletReference(pallet); } public WarehouseLocationResolveResponse resolvePublicLocation(long warehouseId, String locationCode) { if (!StringUtils.hasText(locationCode)) throw new BizException(ResultCode.BAD_REQUEST, "库位码不能为空"); 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(), location.getMaxLoadKg(), location.getStatus()); } private WarehousePalletResolveResponse palletReference(WarehousePalletEntity pallet) { WarehouseLocationEntity location = 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 "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 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 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 (!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()); entity.setFbaWarehouseCode(request.fbaWarehouseCode()); return entity; } private static String generatedPalletCode(String warehouseCode, long palletId) { String boundedWarehouseCode = warehouseCode.length() <= 30 ? warehouseCode : warehouseCode.substring(0, 30); return "PLT-" + boundedWarehouseCode + "-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-" + Long.toUnsignedString(palletId); } private static String defaultLocationCode(String warehouseCode) { return "LOC-" + warehouseCode + "-DEFAULT"; } }