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 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 (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 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 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 (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()); } }