feat: support overseas warehouse locations and A0 overflow
This commit is contained in:
+5
@@ -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;
|
||||||
@@ -2920,6 +2920,7 @@ CREATE TABLE wh_location (
|
|||||||
zone_code varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '仓库分区编码',
|
zone_code varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '仓库分区编码',
|
||||||
location_code varchar(64) 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 '库位类型',
|
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 '最大承重(千克)',
|
max_load_kg decimal(18,3) NOT NULL COMMENT '最大承重(千克)',
|
||||||
status varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用',
|
status varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用',
|
||||||
remark varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
|
remark varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
|
||||||
|
|||||||
+1
@@ -14,6 +14,7 @@ public record InboundPlanResponse(
|
|||||||
String customerName,
|
String customerName,
|
||||||
String orderType,
|
String orderType,
|
||||||
String containerNo,
|
String containerNo,
|
||||||
|
Long overseasWarehouseId,
|
||||||
String overseasWarehouseCode,
|
String overseasWarehouseCode,
|
||||||
String overseasWarehouseName,
|
String overseasWarehouseName,
|
||||||
String status,
|
String status,
|
||||||
|
|||||||
+1
-1
@@ -1124,7 +1124,7 @@ public class InboundPlanService {
|
|||||||
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getSourceOrderId(),
|
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getSourceOrderId(),
|
||||||
sourceOrder == null ? null : sourceOrder.orderNo(),
|
sourceOrder == null ? null : sourceOrder.orderNo(),
|
||||||
sourceOrder == null ? null : sourceOrder.customerName(),
|
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(),
|
authoritativeWarehouse.warehouseCode(), authoritativeWarehouse.warehouseName(),
|
||||||
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), totalGrossWeightKg,
|
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), totalGrossWeightKg,
|
||||||
totalNetWeightKg, plan.getTotalVolumeCbm(),
|
totalNetWeightKg, plan.getTotalVolumeCbm(),
|
||||||
|
|||||||
+42
@@ -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<PdaInventoryMoveResponse> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -45,6 +45,22 @@ public class PdaInventoryMoveService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public PdaInventoryMoveResponse move(long warehouseId, String operatorKey, PdaInventoryMoveRequest request) {
|
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 key = required(request.idempotencyKey(), "Idempotency key is required");
|
||||||
String requestHash = hash(request);
|
String requestHash = hash(request);
|
||||||
PdaInventoryMove existing = moves.find(warehouseId, operatorKey, key);
|
PdaInventoryMove existing = moves.find(warehouseId, operatorKey, key);
|
||||||
@@ -58,6 +74,11 @@ public class PdaInventoryMoveService {
|
|||||||
if (!Set.of("ACTIVE", "IDLE", "OCCUPIED").contains(target.status())) {
|
if (!Set.of("ACTIVE", "IDLE", "OCCUPIED").contains(target.status())) {
|
||||||
throw conflict("Target location is not available");
|
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()
|
boolean recoveringRemoteSuccess = pallet.locationId() == target.locationId()
|
||||||
&& pallet.version() == request.expectedPalletVersion() + 1;
|
&& pallet.version() == request.expectedPalletVersion() + 1;
|
||||||
if (!recoveringRemoteSuccess && pallet.version() != request.expectedPalletVersion()) {
|
if (!recoveringRemoteSuccess && pallet.version() != request.expectedPalletVersion()) {
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ paths:
|
|||||||
'200': { description: 移库结果, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryMoveResponse' } } } }
|
'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' } } } }
|
'404': { description: 来源托盘不存在(统一响应实际 HTTP 200,body.code=A0404), content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } } }
|
||||||
'409': { description: 跨仓、资源或库存版本冲突 }
|
'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:
|
/api/pda/v1/last-leg/inventory/resolve:
|
||||||
post:
|
post:
|
||||||
summary: 扫码查询Token绑定仓库存
|
summary: 扫码查询Token绑定仓库存
|
||||||
@@ -930,6 +938,16 @@ components:
|
|||||||
expectedPalletVersion: { type: integer, minimum: 0 }
|
expectedPalletVersion: { type: integer, minimum: 0 }
|
||||||
idempotencyKey: { type: string, maxLength: 128 }
|
idempotencyKey: { type: string, maxLength: 128 }
|
||||||
reason: { type: string, minLength: 1, maxLength: 500 }
|
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:
|
PalletExpectation:
|
||||||
type: object
|
type: object
|
||||||
required: [palletCode, expectedVersion]
|
required: [palletCode, expectedVersion]
|
||||||
|
|||||||
+1
-1
@@ -433,7 +433,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
|||||||
|
|
||||||
private InboundPlanResponse plan(String planNo, List<InboundPlanResponse.Pallet> pallets) {
|
private InboundPlanResponse plan(String planNo, List<InboundPlanResponse.Pallet> pallets) {
|
||||||
return new InboundPlanResponse(21L, planNo, "100", 11L, "FL-WH-1", "验收客户", "ONE_CLICK_DROPSHIP",
|
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,
|
"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,
|
List.of(), pallets, List.of(), 1, LocalDate.of(2026, 9, 10), "WOOD", false,
|
||||||
false, false, "MANUAL", "自动生成", null, null);
|
false, false, "MANUAL", "自动生成", null, null);
|
||||||
|
|||||||
+1
-1
@@ -3,4 +3,4 @@ package com.baiyue.warehouse.dto;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
public record WarehouseLocationResolveResponse(long locationId, String locationCode, String locationType,
|
public record WarehouseLocationResolveResponse(long locationId, String locationCode, String locationType,
|
||||||
BigDecimal maxLoadKg, String status) { }
|
boolean sharedOverflow, BigDecimal maxLoadKg, String status) { }
|
||||||
|
|||||||
+2
@@ -20,6 +20,8 @@ public class WarehouseLocationEntity extends BaseEntity {
|
|||||||
private String zoneCode;
|
private String zoneCode;
|
||||||
private String locationCode;
|
private String locationCode;
|
||||||
private String locationType;
|
private String locationType;
|
||||||
|
/** A0 is a system-managed shared overflow location; normal locations are exclusive. */
|
||||||
|
private Boolean sharedOverflow;
|
||||||
private BigDecimal maxLoadKg;
|
private BigDecimal maxLoadKg;
|
||||||
private String status;
|
private String status;
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+8
@@ -47,6 +47,14 @@ public class WarehouseResourceRepository {
|
|||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WarehouseLocationEntity findSharedOverflowLocation(long warehouseId) {
|
||||||
|
return locations.selectOne(new LambdaQueryWrapper<WarehouseLocationEntity>()
|
||||||
|
.eq(WarehouseLocationEntity::getCustomerId, 0L)
|
||||||
|
.eq(WarehouseLocationEntity::getWarehouseId, warehouseId)
|
||||||
|
.eq(WarehouseLocationEntity::getLocationCode, "A0")
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
public WarehouseLocationEntity findLocationByCode(long customerId, long warehouseId, String locationCode) {
|
public WarehouseLocationEntity findLocationByCode(long customerId, long warehouseId, String locationCode) {
|
||||||
return locations.selectOne(new LambdaQueryWrapper<WarehouseLocationEntity>()
|
return locations.selectOne(new LambdaQueryWrapper<WarehouseLocationEntity>()
|
||||||
.eq(WarehouseLocationEntity::getCustomerId, customerId)
|
.eq(WarehouseLocationEntity::getCustomerId, customerId)
|
||||||
|
|||||||
+15
-11
@@ -34,8 +34,8 @@ public class WarehouseResourceLockService {
|
|||||||
if (concurrent != null) return idempotentResult(concurrent, request);
|
if (concurrent != null) return idempotentResult(concurrent, request);
|
||||||
throw new BizException(ResultCode.NOT_FOUND, "Pallet or location does not belong to the tenant warehouse");
|
throw new BizException(ResultCode.NOT_FOUND, "Pallet or location does not belong to the tenant warehouse");
|
||||||
}
|
}
|
||||||
boolean virtualLocation = isVirtual(location);
|
boolean nonExclusiveLocation = isNonExclusive(location);
|
||||||
boolean locationAvailable = virtualLocation ? "ACTIVE".equals(location.getStatus()) : "IDLE".equals(location.getStatus());
|
boolean locationAvailable = nonExclusiveLocation ? "ACTIVE".equals(location.getStatus()) : "IDLE".equals(location.getStatus());
|
||||||
if (!"IDLE".equals(pallet.getStatus()) || !locationAvailable) {
|
if (!"IDLE".equals(pallet.getStatus()) || !locationAvailable) {
|
||||||
WarehouseResourceLockEntity concurrent =
|
WarehouseResourceLockEntity concurrent =
|
||||||
repository.findByReservationKey(request.customerId(), request.reservationKey());
|
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) {
|
if (repository.occupyPallet(request.customerId(), request.warehouseId(), request.palletId(), request.locationId()) != 1) {
|
||||||
return concurrentIdempotentOrConflict(request);
|
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");
|
throw new BizException(ResultCode.CONFLICT, "Location is no longer IDLE");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +68,9 @@ public class WarehouseResourceLockService {
|
|||||||
WarehouseResourceLockEntity lock = requireLock(customerId, lockId);
|
WarehouseResourceLockEntity lock = requireLock(customerId, lockId);
|
||||||
if ("RELEASED".equals(lock.getStatus())) return WarehouseResourceLockResponse.from(lock);
|
if ("RELEASED".equals(lock.getStatus())) return WarehouseResourceLockResponse.from(lock);
|
||||||
requireActive(lock);
|
requireActive(lock);
|
||||||
boolean virtualLocation = isVirtualLockLocation(lock);
|
boolean nonExclusiveLocation = isNonExclusiveLockLocation(lock);
|
||||||
int palletResult = virtualLocation ? repository.releasePalletAtVirtualLocation(lock) : repository.releasePallet(lock);
|
int palletResult = nonExclusiveLocation ? repository.releasePalletAtVirtualLocation(lock) : repository.releasePallet(lock);
|
||||||
if (palletResult != 1 || (!virtualLocation && repository.releaseLocation(lock) != 1)
|
if (palletResult != 1 || (!nonExclusiveLocation && repository.releaseLocation(lock) != 1)
|
||||||
|| repository.close(lock, "RELEASED") != 1) {
|
|| repository.close(lock, "RELEASED") != 1) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "Resource lock state changed concurrently");
|
throw new BizException(ResultCode.CONFLICT, "Resource lock state changed concurrently");
|
||||||
}
|
}
|
||||||
@@ -84,9 +84,9 @@ public class WarehouseResourceLockService {
|
|||||||
WarehouseResourceLockEntity lock = requireLock(customerId, lockId);
|
WarehouseResourceLockEntity lock = requireLock(customerId, lockId);
|
||||||
if ("SCRAPPED".equals(lock.getStatus())) return WarehouseResourceLockResponse.from(lock);
|
if ("SCRAPPED".equals(lock.getStatus())) return WarehouseResourceLockResponse.from(lock);
|
||||||
requireActive(lock);
|
requireActive(lock);
|
||||||
boolean virtualLocation = isVirtualLockLocation(lock);
|
boolean nonExclusiveLocation = isNonExclusiveLockLocation(lock);
|
||||||
int palletResult = virtualLocation ? repository.scrapPalletAtVirtualLocation(lock) : repository.scrapPallet(lock);
|
int palletResult = nonExclusiveLocation ? repository.scrapPalletAtVirtualLocation(lock) : repository.scrapPallet(lock);
|
||||||
if (palletResult != 1 || (!virtualLocation && repository.releaseLocation(lock) != 1)
|
if (palletResult != 1 || (!nonExclusiveLocation && repository.releaseLocation(lock) != 1)
|
||||||
|| repository.close(lock, "SCRAPPED") != 1) {
|
|| repository.close(lock, "SCRAPPED") != 1) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "Resource lock state changed concurrently");
|
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());
|
WarehouseLocationEntity location = repository.findLocation(lock.getCustomerId(), lock.getWarehouseId(), lock.getLocationId());
|
||||||
if (location == null) throw new BizException(ResultCode.NOT_FOUND, "Resource lock location not found");
|
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) {
|
private static boolean isVirtual(WarehouseLocationEntity location) {
|
||||||
return "VIRTUAL".equals(location.getLocationType());
|
return "VIRTUAL".equals(location.getLocationType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isNonExclusive(WarehouseLocationEntity location) {
|
||||||
|
return isVirtual(location) || WarehouseResourceService.isSharedOverflow(location);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-1
@@ -25,6 +25,7 @@ import org.springframework.util.StringUtils;
|
|||||||
@Service
|
@Service
|
||||||
public class WarehouseResourceService {
|
public class WarehouseResourceService {
|
||||||
public static final long PUBLIC_RESOURCE_SCOPE = 0L;
|
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> LOCATION_STATUSES = Set.of("ACTIVE", "IDLE", "OCCUPIED", "INACTIVE");
|
||||||
private static final Set<String> PALLET_STATUSES = Set.of("IDLE", "OCCUPIED", "SCRAPPED");
|
private static final Set<String> PALLET_STATUSES = Set.of("IDLE", "OCCUPIED", "SCRAPPED");
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ public class WarehouseResourceService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void updateLocation(long customerId, long warehouseId, long id, LocationRequests.SaveRequest request) {
|
public void updateLocation(long customerId, long warehouseId, long id, LocationRequests.SaveRequest request) {
|
||||||
WarehouseLocationEntity existing = requireLocation(customerId, warehouseId, id);
|
WarehouseLocationEntity existing = requireLocation(customerId, warehouseId, id);
|
||||||
|
if (isSharedOverflow(existing)) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "系统共享溢出库位 A0 不允许编辑");
|
||||||
|
}
|
||||||
if ("VIRTUAL".equals(existing.getLocationType())
|
if ("VIRTUAL".equals(existing.getLocationType())
|
||||||
&& (!"VIRTUAL".equals(request.locationType()) || !"ACTIVE".equals(request.status())
|
&& (!"VIRTUAL".equals(request.locationType()) || !"ACTIVE".equals(request.status())
|
||||||
|| !existing.getLocationCode().equals(request.locationCode()))) {
|
|| !existing.getLocationCode().equals(request.locationCode()))) {
|
||||||
@@ -62,12 +66,16 @@ public class WarehouseResourceService {
|
|||||||
public PageResult<WarehouseLocationEntity> locations(long customerId, long warehouseId,
|
public PageResult<WarehouseLocationEntity> locations(long customerId, long warehouseId,
|
||||||
LocationRequests.QueryRequest query) {
|
LocationRequests.QueryRequest query) {
|
||||||
requireWarehouse(customerId, warehouseId);
|
requireWarehouse(customerId, warehouseId);
|
||||||
|
if (customerId == PUBLIC_RESOURCE_SCOPE) ensureSharedOverflowLocation(warehouseId);
|
||||||
return repository.pageLocations(customerId, warehouseId, query);
|
return repository.pageLocations(customerId, warehouseId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteLocation(long customerId, long warehouseId, long id) {
|
public void deleteLocation(long customerId, long warehouseId, long id) {
|
||||||
WarehouseLocationEntity location = requireLocation(customerId, warehouseId, id);
|
WarehouseLocationEntity location = requireLocation(customerId, warehouseId, id);
|
||||||
|
if (isSharedOverflow(location)) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "系统共享溢出库位 A0 不允许删除");
|
||||||
|
}
|
||||||
if ("VIRTUAL".equals(location.getLocationType())) {
|
if ("VIRTUAL".equals(location.getLocationType())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "Default virtual location cannot be deleted");
|
throw new BizException(ResultCode.CONFLICT, "Default virtual location cannot be deleted");
|
||||||
}
|
}
|
||||||
@@ -192,6 +200,40 @@ public class WarehouseResourceService {
|
|||||||
return resolved;
|
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
|
@Transactional
|
||||||
public void updatePallet(long customerId, long warehouseId, long id, PalletRequests.SaveRequest request) {
|
public void updatePallet(long customerId, long warehouseId, long id, PalletRequests.SaveRequest request) {
|
||||||
WarehousePalletEntity existing = requirePallet(customerId, warehouseId, id);
|
WarehousePalletEntity existing = requirePallet(customerId, warehouseId, id);
|
||||||
@@ -244,6 +286,9 @@ public class WarehouseResourceService {
|
|||||||
|
|
||||||
public WarehouseLocationResolveResponse resolvePublicLocation(long warehouseId, String locationCode) {
|
public WarehouseLocationResolveResponse resolvePublicLocation(long warehouseId, String locationCode) {
|
||||||
if (!StringUtils.hasText(locationCode)) throw new BizException(ResultCode.BAD_REQUEST, "库位码不能为空");
|
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,
|
WarehouseLocationEntity location = repository.findLocationByCode(PUBLIC_RESOURCE_SCOPE, warehouseId,
|
||||||
locationCode.trim());
|
locationCode.trim());
|
||||||
if (location == null) throw new BizException(ResultCode.NOT_FOUND, "库位码不存在或不属于当前仓库");
|
if (location == null) throw new BizException(ResultCode.NOT_FOUND, "库位码不存在或不属于当前仓库");
|
||||||
@@ -258,7 +303,7 @@ public class WarehouseResourceService {
|
|||||||
|
|
||||||
private static WarehouseLocationResolveResponse locationReference(WarehouseLocationEntity location) {
|
private static WarehouseLocationResolveResponse locationReference(WarehouseLocationEntity location) {
|
||||||
return new WarehouseLocationResolveResponse(location.getId(), location.getLocationCode(),
|
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) {
|
private WarehousePalletResolveResponse palletReference(WarehousePalletEntity pallet) {
|
||||||
@@ -345,6 +390,10 @@ public class WarehouseResourceService {
|
|||||||
|
|
||||||
private void validateLocation(LocationRequests.SaveRequest request, long customerId,
|
private void validateLocation(LocationRequests.SaveRequest request, long customerId,
|
||||||
long warehouseId, Long excludedId) {
|
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())) {
|
if (!LOCATION_STATUSES.contains(request.status())) {
|
||||||
throw new BizException("库位状态仅支持 ACTIVE、IDLE、OCCUPIED、INACTIVE");
|
throw new BizException("库位状态仅支持 ACTIVE、IDLE、OCCUPIED、INACTIVE");
|
||||||
}
|
}
|
||||||
@@ -412,4 +461,10 @@ public class WarehouseResourceService {
|
|||||||
private static String defaultLocationCode(String warehouseCode) {
|
private static String defaultLocationCode(String warehouseCode) {
|
||||||
return "LOC-" + warehouseCode + "-DEFAULT";
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS wh_location (
|
|||||||
zone_code VARCHAR(50) NOT NULL COMMENT '仓库分区编码',
|
zone_code VARCHAR(50) NOT NULL COMMENT '仓库分区编码',
|
||||||
location_code VARCHAR(64) NOT NULL COMMENT '仓库内唯一库位编码',
|
location_code VARCHAR(64) NOT NULL COMMENT '仓库内唯一库位编码',
|
||||||
location_type VARCHAR(32) 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 '最大承重(千克)',
|
max_load_kg DECIMAL(18,3) NOT NULL COMMENT '最大承重(千克)',
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用',
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用',
|
||||||
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
@@ -58,6 +59,14 @@ PREPARE pallet_fba_warehouse_stmt FROM @ddl;
|
|||||||
EXECUTE pallet_fba_warehouse_stmt;
|
EXECUTE pallet_fba_warehouse_stmt;
|
||||||
DEALLOCATE PREPARE 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 (
|
CREATE TABLE IF NOT EXISTS wh_resource_lock (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
|
||||||
customer_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 zone_code VARCHAR(50) NOT NULL COMMENT '仓库分区编码',
|
||||||
MODIFY COLUMN location_code VARCHAR(64) NOT NULL COMMENT '仓库内唯一库位编码',
|
MODIFY COLUMN location_code VARCHAR(64) NOT NULL COMMENT '仓库内唯一库位编码',
|
||||||
MODIFY COLUMN location_type VARCHAR(32) 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 max_load_kg DECIMAL(18,3) NOT NULL COMMENT '最大承重(千克)',
|
||||||
MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用',
|
MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '库位状态:ACTIVE启用、INACTIVE停用',
|
||||||
MODIFY COLUMN remark VARCHAR(500) NULL COMMENT '备注',
|
MODIFY COLUMN remark VARCHAR(500) NULL COMMENT '备注',
|
||||||
|
|||||||
Reference in New Issue
Block a user