diff --git a/deploy/sql/migrations/overseas/baiyue_lastlag_warehouse/V1__add_shared_overflow_location.sql b/deploy/sql/migrations/overseas/baiyue_lastlag_warehouse/V1__add_shared_overflow_location.sql new file mode 100644 index 000000000..433e44e15 --- /dev/null +++ b/deploy/sql/migrations/overseas/baiyue_lastlag_warehouse/V1__add_shared_overflow_location.sql @@ -0,0 +1,5 @@ +-- 海外仓库位 A0 共享溢出位:普通库位独占,A0 允许多托盘共享。 +ALTER TABLE wh_location + ADD COLUMN shared_overflow TINYINT(1) NOT NULL DEFAULT 0 + COMMENT '是否系统共享溢出库位(A0)' + AFTER location_type; diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 40a84bd1b..5f3e46564 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -2920,6 +2920,7 @@ CREATE TABLE wh_location ( zone_code varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '仓库分区编码', location_code varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '仓库内唯一库位编码', location_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '库位类型', + shared_overflow tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否系统共享溢出库位(A0)', max_load_kg decimal(18,3) NOT NULL COMMENT '最大承重(千克)', status varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用', remark varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java index 8bcd713b7..460d04827 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java @@ -14,6 +14,7 @@ public record InboundPlanResponse( String customerName, String orderType, String containerNo, + Long overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName, String status, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java index 31483e4be..61700639a 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java @@ -1124,7 +1124,7 @@ public class InboundPlanService { return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getSourceOrderId(), sourceOrder == null ? null : sourceOrder.orderNo(), sourceOrder == null ? null : sourceOrder.customerName(), - sourceOrder == null ? null : sourceOrder.orderType(), plan.getContainerNo(), + sourceOrder == null ? null : sourceOrder.orderType(), plan.getContainerNo(), authoritativeWarehouse.warehouseId(), authoritativeWarehouse.warehouseCode(), authoritativeWarehouse.warehouseName(), plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), totalGrossWeightKg, totalNetWeightKg, plan.getTotalVolumeCbm(), diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/controller/PlatformInventoryMoveController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/controller/PlatformInventoryMoveController.java new file mode 100644 index 000000000..b77eee11b --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/controller/PlatformInventoryMoveController.java @@ -0,0 +1,42 @@ +package com.baiyue.lastleg.pda.move.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.lastleg.pda.move.dto.PlatformInventoryMoveRequest; +import com.baiyue.lastleg.pda.move.dto.PdaInventoryMoveResponse; +import com.baiyue.lastleg.pda.move.service.PdaInventoryMoveService; +import com.baiyue.lastleg.security.OverseasPermissions; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +/** Browser/platform entry point; the PDA entry point remains restricted to warehouse operators. */ +@RestController +@RequestMapping("/api/platform/v1/last-leg/inventory-moves") +@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EDIT + "')") +public class PlatformInventoryMoveController { + @Resource private PdaInventoryMoveService service; + + @PostMapping + public ApiResponse move(@AuthenticationPrincipal AuthenticatedUser user, + @Valid @RequestBody PlatformInventoryMoveRequest request) { + requirePlatform(user); + String operator = user.userId() == null ? user.username() : String.valueOf(user.userId()); + return ApiResponse.success(service.movePlatform(request.overseasWarehouseId(), operator, + request.asPdaRequest())); + } + + private static void requirePlatform(AuthenticatedUser user) { + if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform identity is required"); + } + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PlatformInventoryMoveRequest.java b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PlatformInventoryMoveRequest.java new file mode 100644 index 000000000..fa7954687 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PlatformInventoryMoveRequest.java @@ -0,0 +1,21 @@ +package com.baiyue.lastleg.pda.move.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; + +/** Platform/admin variant of the pallet put-away command. */ +public record PlatformInventoryMoveRequest( + @Positive long overseasWarehouseId, + @NotBlank @Size(max = 128) String sourcePalletCode, + @NotBlank @Size(max = 128) String targetLocationCode, + @PositiveOrZero int expectedPalletVersion, + @NotBlank @Size(max = 128) String idempotencyKey, + @NotBlank @Size(max = 500) String reason +) { + public PdaInventoryMoveRequest asPdaRequest() { + return new PdaInventoryMoveRequest(sourcePalletCode, targetLocationCode, + expectedPalletVersion, idempotencyKey, reason); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java index f5d5263f9..0bae11cf7 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java @@ -45,6 +45,22 @@ public class PdaInventoryMoveService { @Transactional public PdaInventoryMoveResponse move(long warehouseId, String operatorKey, PdaInventoryMoveRequest request) { + return moveInternal(warehouseId, operatorKey, request); + } + + /** + * Platform/admin put-away uses the same command, idempotency record and physical + * transition as the PDA flow. Keeping one implementation prevents the two entry + * points from drifting on A0, virtual-location and inventory-position semantics. + */ + @Transactional + public PdaInventoryMoveResponse movePlatform(long warehouseId, String operatorKey, + PdaInventoryMoveRequest request) { + return moveInternal(warehouseId, "PLATFORM:" + required(operatorKey, "Operator is required"), request); + } + + private PdaInventoryMoveResponse moveInternal(long warehouseId, String operatorKey, + PdaInventoryMoveRequest request) { String key = required(request.idempotencyKey(), "Idempotency key is required"); String requestHash = hash(request); PdaInventoryMove existing = moves.find(warehouseId, operatorKey, key); @@ -58,6 +74,11 @@ public class PdaInventoryMoveService { if (!Set.of("ACTIVE", "IDLE", "OCCUPIED").contains(target.status())) { throw conflict("Target location is not available"); } + // A virtual receiving location is an intake staging position, not a put-away + // destination. A0 is deliberately allowed as a shared overflow location. + if ("VIRTUAL".equalsIgnoreCase(target.locationType())) { + throw conflict("Virtual receiving location cannot be used as a put-away target"); + } boolean recoveringRemoteSuccess = pallet.locationId() == target.locationId() && pallet.version() == request.expectedPalletVersion() + 1; if (!recoveringRemoteSuccess && pallet.version() != request.expectedPalletVersion()) { diff --git a/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml index f1ae643ca..bc6fdff02 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml @@ -129,6 +129,14 @@ paths: '200': { description: 移库结果, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryMoveResponse' } } } } '404': { description: 来源托盘不存在(统一响应实际 HTTP 200,body.code=A0404), content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } } } '409': { description: 跨仓、资源或库存版本冲突 } + /api/platform/v1/last-leg/inventory-moves: + post: + summary: 平台发起海外仓整托移库 + description: 平台按海外仓业务标识执行普通库位或 A0 共享溢出位移库;目标虚拟收货位、跨仓和版本冲突均拒绝。 + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/PlatformInventoryMove' } } } } + responses: + '200': { description: 移库结果, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryMoveResponse' } } } } + '409': { description: 物理资源或本地版本冲突 } /api/pda/v1/last-leg/inventory/resolve: post: summary: 扫码查询Token绑定仓库存 @@ -930,6 +938,16 @@ components: expectedPalletVersion: { type: integer, minimum: 0 } idempotencyKey: { type: string, maxLength: 128 } reason: { type: string, minLength: 1, maxLength: 500 } + PlatformInventoryMove: + type: object + required: [overseasWarehouseId, sourcePalletCode, targetLocationCode, expectedPalletVersion, idempotencyKey, reason] + properties: + overseasWarehouseId: { type: string, pattern: '^[0-9]+$' } + sourcePalletCode: { type: string, maxLength: 128 } + targetLocationCode: { type: string, maxLength: 128 } + expectedPalletVersion: { type: integer, format: int64, minimum: 0 } + idempotencyKey: { type: string, maxLength: 128 } + reason: { type: string, minLength: 1, maxLength: 500 } PalletExpectation: type: object required: [palletCode, expectedVersion] diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java index fc143f7b2..5515f9dd3 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java @@ -433,7 +433,7 @@ class ContainerInboundPlanConversionCoordinatorTest { private InboundPlanResponse plan(String planNo, List pallets) { return new InboundPlanResponse(21L, planNo, "100", 11L, "FL-WH-1", "验收客户", "ONE_CLICK_DROPSHIP", - "MSCU6639870", "US-LAX-01", + "MSCU6639870", 7L, "US-LAX-01", "Los Angeles", "PENDING_ASSIGNMENT", 1, 1, BigDecimal.TEN, new BigDecimal("8"), BigDecimal.ONE, 10, 0, List.of(), pallets, List.of(), 1, LocalDate.of(2026, 9, 10), "WOOD", false, false, false, "MANUAL", "自动生成", null, null); diff --git a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/dto/WarehouseLocationResolveResponse.java b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/dto/WarehouseLocationResolveResponse.java index 571c15925..ba4762bc7 100644 --- a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/dto/WarehouseLocationResolveResponse.java +++ b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/dto/WarehouseLocationResolveResponse.java @@ -3,4 +3,4 @@ package com.baiyue.warehouse.dto; import java.math.BigDecimal; public record WarehouseLocationResolveResponse(long locationId, String locationCode, String locationType, - BigDecimal maxLoadKg, String status) { } + boolean sharedOverflow, BigDecimal maxLoadKg, String status) { } diff --git a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/entity/WarehouseLocationEntity.java b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/entity/WarehouseLocationEntity.java index 9027321f5..97ab5671a 100644 --- a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/entity/WarehouseLocationEntity.java +++ b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/entity/WarehouseLocationEntity.java @@ -20,6 +20,8 @@ public class WarehouseLocationEntity extends BaseEntity { private String zoneCode; private String locationCode; private String locationType; + /** A0 is a system-managed shared overflow location; normal locations are exclusive. */ + private Boolean sharedOverflow; private BigDecimal maxLoadKg; private String status; private String remark; diff --git a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/repository/WarehouseResourceRepository.java b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/repository/WarehouseResourceRepository.java index ac973e401..69c4e5cff 100644 --- a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/repository/WarehouseResourceRepository.java +++ b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/repository/WarehouseResourceRepository.java @@ -47,6 +47,14 @@ public class WarehouseResourceRepository { .last("LIMIT 1")); } + public WarehouseLocationEntity findSharedOverflowLocation(long warehouseId) { + return locations.selectOne(new LambdaQueryWrapper() + .eq(WarehouseLocationEntity::getCustomerId, 0L) + .eq(WarehouseLocationEntity::getWarehouseId, warehouseId) + .eq(WarehouseLocationEntity::getLocationCode, "A0") + .last("LIMIT 1")); + } + public WarehouseLocationEntity findLocationByCode(long customerId, long warehouseId, String locationCode) { return locations.selectOne(new LambdaQueryWrapper() .eq(WarehouseLocationEntity::getCustomerId, customerId) diff --git a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceLockService.java b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceLockService.java index cf00e7907..3f0be9c31 100644 --- a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceLockService.java +++ b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceLockService.java @@ -34,8 +34,8 @@ public class WarehouseResourceLockService { if (concurrent != null) return idempotentResult(concurrent, request); throw new BizException(ResultCode.NOT_FOUND, "Pallet or location does not belong to the tenant warehouse"); } - boolean virtualLocation = isVirtual(location); - boolean locationAvailable = virtualLocation ? "ACTIVE".equals(location.getStatus()) : "IDLE".equals(location.getStatus()); + boolean nonExclusiveLocation = isNonExclusive(location); + boolean locationAvailable = nonExclusiveLocation ? "ACTIVE".equals(location.getStatus()) : "IDLE".equals(location.getStatus()); if (!"IDLE".equals(pallet.getStatus()) || !locationAvailable) { WarehouseResourceLockEntity concurrent = repository.findByReservationKey(request.customerId(), request.reservationKey()); @@ -45,7 +45,7 @@ public class WarehouseResourceLockService { if (repository.occupyPallet(request.customerId(), request.warehouseId(), request.palletId(), request.locationId()) != 1) { return concurrentIdempotentOrConflict(request); } - if (!virtualLocation && repository.occupyLocation(request.customerId(), request.warehouseId(), request.locationId()) != 1) { + if (!nonExclusiveLocation && repository.occupyLocation(request.customerId(), request.warehouseId(), request.locationId()) != 1) { throw new BizException(ResultCode.CONFLICT, "Location is no longer IDLE"); } @@ -68,9 +68,9 @@ public class WarehouseResourceLockService { WarehouseResourceLockEntity lock = requireLock(customerId, lockId); if ("RELEASED".equals(lock.getStatus())) return WarehouseResourceLockResponse.from(lock); requireActive(lock); - boolean virtualLocation = isVirtualLockLocation(lock); - int palletResult = virtualLocation ? repository.releasePalletAtVirtualLocation(lock) : repository.releasePallet(lock); - if (palletResult != 1 || (!virtualLocation && repository.releaseLocation(lock) != 1) + boolean nonExclusiveLocation = isNonExclusiveLockLocation(lock); + int palletResult = nonExclusiveLocation ? repository.releasePalletAtVirtualLocation(lock) : repository.releasePallet(lock); + if (palletResult != 1 || (!nonExclusiveLocation && repository.releaseLocation(lock) != 1) || repository.close(lock, "RELEASED") != 1) { throw new BizException(ResultCode.CONFLICT, "Resource lock state changed concurrently"); } @@ -84,9 +84,9 @@ public class WarehouseResourceLockService { WarehouseResourceLockEntity lock = requireLock(customerId, lockId); if ("SCRAPPED".equals(lock.getStatus())) return WarehouseResourceLockResponse.from(lock); requireActive(lock); - boolean virtualLocation = isVirtualLockLocation(lock); - int palletResult = virtualLocation ? repository.scrapPalletAtVirtualLocation(lock) : repository.scrapPallet(lock); - if (palletResult != 1 || (!virtualLocation && repository.releaseLocation(lock) != 1) + boolean nonExclusiveLocation = isNonExclusiveLockLocation(lock); + int palletResult = nonExclusiveLocation ? repository.scrapPalletAtVirtualLocation(lock) : repository.scrapPallet(lock); + if (palletResult != 1 || (!nonExclusiveLocation && repository.releaseLocation(lock) != 1) || repository.close(lock, "SCRAPPED") != 1) { throw new BizException(ResultCode.CONFLICT, "Resource lock state changed concurrently"); } @@ -150,13 +150,17 @@ public class WarehouseResourceLockService { } - private boolean isVirtualLockLocation(WarehouseResourceLockEntity lock) { + private boolean isNonExclusiveLockLocation(WarehouseResourceLockEntity lock) { WarehouseLocationEntity location = repository.findLocation(lock.getCustomerId(), lock.getWarehouseId(), lock.getLocationId()); if (location == null) throw new BizException(ResultCode.NOT_FOUND, "Resource lock location not found"); - return isVirtual(location); + return isNonExclusive(location); } private static boolean isVirtual(WarehouseLocationEntity location) { return "VIRTUAL".equals(location.getLocationType()); } + + private static boolean isNonExclusive(WarehouseLocationEntity location) { + return isVirtual(location) || WarehouseResourceService.isSharedOverflow(location); + } } diff --git a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceService.java b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceService.java index 2c6741aed..1036dbdb2 100644 --- a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceService.java +++ b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehouseResourceService.java @@ -25,6 +25,7 @@ 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"); @@ -43,6 +44,9 @@ public class WarehouseResourceService { @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()))) { @@ -62,12 +66,16 @@ public class WarehouseResourceService { 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"); } @@ -192,6 +200,40 @@ public class WarehouseResourceService { 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.setZoneCode("OVERFLOW"); + 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); @@ -244,6 +286,9 @@ public class WarehouseResourceService { 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, "库位码不存在或不属于当前仓库"); @@ -258,7 +303,7 @@ public class WarehouseResourceService { private static WarehouseLocationResolveResponse locationReference(WarehouseLocationEntity location) { return new WarehouseLocationResolveResponse(location.getId(), location.getLocationCode(), - location.getLocationType(), location.getMaxLoadKg(), location.getStatus()); + location.getLocationType(), isSharedOverflow(location), location.getMaxLoadKg(), location.getStatus()); } private WarehousePalletResolveResponse palletReference(WarehousePalletEntity pallet) { @@ -345,6 +390,10 @@ public class WarehouseResourceService { 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"); } @@ -412,4 +461,10 @@ public class WarehouseResourceService { 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()); + } } diff --git a/warehouse-overseas-service/src/main/resources/db/warehouse.sql b/warehouse-overseas-service/src/main/resources/db/warehouse.sql index fa42e96cb..874e1a9c9 100644 --- a/warehouse-overseas-service/src/main/resources/db/warehouse.sql +++ b/warehouse-overseas-service/src/main/resources/db/warehouse.sql @@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS wh_location ( zone_code VARCHAR(50) NOT NULL COMMENT '仓库分区编码', location_code VARCHAR(64) NOT NULL COMMENT '仓库内唯一库位编码', location_type VARCHAR(32) NOT NULL COMMENT '库位类型', + shared_overflow TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否系统共享溢出库位(A0)', max_load_kg DECIMAL(18,3) NOT NULL COMMENT '最大承重(千克)', status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用', remark VARCHAR(500) DEFAULT NULL COMMENT '备注', @@ -58,6 +59,14 @@ PREPARE pallet_fba_warehouse_stmt FROM @ddl; EXECUTE pallet_fba_warehouse_stmt; DEALLOCATE PREPARE pallet_fba_warehouse_stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() + AND table_name='wh_location' AND column_name='shared_overflow'), + 'SELECT 1', + 'ALTER TABLE wh_location ADD COLUMN shared_overflow TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否系统共享溢出库位(A0)'' AFTER location_type')); +PREPARE location_shared_overflow_stmt FROM @ddl; +EXECUTE location_shared_overflow_stmt; +DEALLOCATE PREPARE location_shared_overflow_stmt; + CREATE TABLE IF NOT EXISTS wh_resource_lock ( id BIGINT UNSIGNED NOT NULL COMMENT '主键ID', customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户租户ID', @@ -91,6 +100,7 @@ ALTER TABLE wh_location MODIFY COLUMN zone_code VARCHAR(50) NOT NULL COMMENT '仓库分区编码', MODIFY COLUMN location_code VARCHAR(64) NOT NULL COMMENT '仓库内唯一库位编码', MODIFY COLUMN location_type VARCHAR(32) NOT NULL COMMENT '库位类型', + MODIFY COLUMN shared_overflow TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否系统共享溢出库位(A0)', MODIFY COLUMN max_load_kg DECIMAL(18,3) NOT NULL COMMENT '最大承重(千克)', MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用', MODIFY COLUMN remark VARCHAR(500) NULL COMMENT '备注',