fix: bind overseas receiving to shared warehouse

This commit is contained in:
2026-08-24 21:46:19 +08:00
parent 3607bebbfd
commit 8cc4c6f6da
61 changed files with 774 additions and 118 deletions
@@ -4,13 +4,16 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.warehouse.dto.WarehouseResourceLockRequests;
import com.baiyue.warehouse.dto.WarehouseResourceLockResponse;
import com.baiyue.warehouse.dto.WarehousePalletScrapResponse;
import com.baiyue.warehouse.dto.SharedWarehouseReference;
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
import com.baiyue.warehouse.security.InternalServiceTokenGuard;
import com.baiyue.warehouse.service.WarehouseResourceLockService;
import com.baiyue.warehouse.service.WarehouseResourceService;
import com.baiyue.warehouse.service.WarehouseApplicationService;
import jakarta.validation.Valid;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
@@ -22,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
public class InternalWarehouseResourceLockController {
@Resource private WarehouseResourceLockService service;
@Resource private WarehouseResourceService resourceService;
@Resource private WarehouseApplicationService warehouses;
@Resource private InternalServiceTokenGuard tokenGuard;
@PostMapping
@@ -67,4 +71,12 @@ public class InternalWarehouseResourceLockController {
return ApiResponse.success(resourceService.ensureDefaultVirtualLocation(
request.customerId(), request.warehouseId()));
}
@GetMapping("/shared-warehouses/{warehouseId}")
public ApiResponse<SharedWarehouseReference> sharedWarehouse(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@PathVariable long warehouseId) {
tokenGuard.verify(token);
return ApiResponse.success(warehouses.activeSharedReference(warehouseId));
}
}
@@ -164,8 +164,8 @@ public class PlatformWarehouseController {
}
private static void requireTenant(Long customerId) {
if (customerId == null || customerId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "Target tenant is required / 必须指定有效目标租户");
if (customerId == null || customerId < 0) {
throw new BizException(ResultCode.BAD_REQUEST, "Target customer scope is required / 必须指定有效客户或平台共享范围");
}
}
}
@@ -0,0 +1,4 @@
package com.baiyue.warehouse.dto;
public record SharedWarehouseReference(long warehouseId, String warehouseCode,
String warehouseName, String status) { }
@@ -8,7 +8,7 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
@@ -96,7 +96,7 @@ public final class WarehouseRequests {
@Schema(name = "WarehouseQueryRequest", description = "Warehouse page query / 仓库分页查询条件")
public record QueryRequest(
@Schema(description = "Optional target customer ID for platform queries; merchant queries always use the authenticated customer / 平台查询可选目标客户 ID,商户查询始终使用认证客户", example = "10001", nullable = true)
@Positive Long customerId,
@PositiveOrZero Long customerId,
@Schema(description = "Page number, starting at 1 / 页码,从 1 开始", example = "1", defaultValue = "1", minimum = "1")
@Min(1) long page,
@Schema(description = "Page size / 每页条数", example = "20", defaultValue = "20", minimum = "1", maximum = "200")
@@ -7,6 +7,7 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
import com.baiyue.warehouse.dto.WarehouseRequests;
import com.baiyue.warehouse.dto.SharedWarehouseReference;
import com.baiyue.warehouse.entity.ServiceCapabilityEntity;
import com.baiyue.warehouse.entity.WarehouseAddressEntity;
import com.baiyue.warehouse.entity.WarehouseEntity;
@@ -36,6 +37,7 @@ public class WarehouseApplicationService {
captureBefore = false
)
public Long create(WarehouseRequests.SaveRequest request) {
validateSharedWarehouse(request);
ensureCodeUnique(request.customerId(), request.warehouseCode(), null);
WarehouseEntity warehouse = toEntity(request);
repository.insertWarehouse(warehouse);
@@ -72,6 +74,7 @@ public class WarehouseApplicationService {
if (request.customerId() == null || customerId != request.customerId()) {
throw new BizException(ResultCode.FORBIDDEN, "租户不匹配");
}
validateSharedWarehouse(request);
WarehouseEntity old = owned(customerId, id);
ensureCodeUnique(customerId, request.warehouseCode(), id);
@@ -98,6 +101,29 @@ public class WarehouseApplicationService {
return new WarehouseDetailResponse(warehouse, addresses, capabilities);
}
public SharedWarehouseReference activeSharedReference(long warehouseId) {
WarehouseEntity warehouse = repository.findOwned(0L, warehouseId);
boolean receiving = warehouse != null && repository.findCapabilities(0L, warehouseId).stream()
.anyMatch(value -> "RECEIVING".equals(value.getCapabilityCode()) && Boolean.TRUE.equals(value.getEnabled()));
if (warehouse == null || !"ACTIVE".equals(warehouse.getStatus())
|| !"OVERSEAS".equals(warehouse.getWarehouseType()) || !receiving) {
throw new BizException(ResultCode.NOT_FOUND, "Active shared overseas warehouse not found");
}
return new SharedWarehouseReference(warehouse.getId(), warehouse.getWarehouseCode(),
warehouse.getWarehouseName(), warehouse.getStatus());
}
private void validateSharedWarehouse(WarehouseRequests.SaveRequest request) {
if (request.customerId() == null || request.customerId() != 0L) return;
boolean receiving = request.capabilities() != null && request.capabilities().stream()
.anyMatch(value -> "RECEIVING".equals(value.capabilityCode())
&& !Boolean.FALSE.equals(value.enabled()));
if (!"OVERSEAS".equals(request.warehouseType()) || !receiving) {
throw new BizException(ResultCode.BAD_REQUEST,
"Shared physical warehouse must be OVERSEAS with RECEIVING enabled");
}
}
@Transactional
@BusinessAudit(
event = "WAREHOUSE_STATUS_CHANGE",
@@ -61,6 +61,22 @@ class PlatformWarehouseControllerTest {
assertTrue(operation.description().contains("customerId"));
}
@Test
void platformCanCreateSharedWarehouseInCustomerZeroScope() {
WarehouseApplicationService service = mock(WarehouseApplicationService.class);
PlatformWarehouseController controller = new PlatformWarehouseController();
ReflectionTestUtils.setField(controller, "service", service);
WarehouseRequests.AddressRequest address = new WarehouseRequests.AddressRequest(
"WAREHOUSE", "US", "CA", "Los Angeles", null, "90001", "1 Shared Ave", null, null, null);
WarehouseRequests.SaveRequest request = new WarehouseRequests.SaveRequest(0L, "SHARED-LAX", "Shared LAX",
"OVERSEAS", "US", "America/Los_Angeles", null, null, null, "ACTIVE", null, address,
List.of(new WarehouseRequests.CapabilityRequest("RECEIVING", "收货", true, null, null)));
when(service.create(request)).thenReturn(900L);
assertEquals(900L, controller.create(platform(), request).data());
verify(service).create(request);
}
private static AuthenticatedUser platform() {
return new AuthenticatedUser(1L, 0L, 0L, 1L, "operator", "PLATFORM", "WEB", Set.of());
}
@@ -3,6 +3,8 @@ package com.baiyue.warehouse.service;
import com.baiyue.warehouse.dto.WarehouseRequests;
import com.baiyue.warehouse.entity.WarehouseEntity;
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
import com.baiyue.warehouse.entity.ServiceCapabilityEntity;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.repository.WarehouseRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -15,6 +17,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
@@ -59,6 +62,31 @@ class WarehouseApplicationServiceTest {
verify(repository).pageAll(query);
}
@Test
void sharedReceivingWarehouseMustBeOverseasAndResolvesCanonicalReference() {
WarehouseRequests.SaveRequest shared = sharedRequest("OVERSEAS", true);
doAnswer(call -> { ((WarehouseEntity) call.getArgument(0)).setId(900L); return null; })
.when(repository).insertWarehouse(any());
assertThat(service.create(shared)).isEqualTo(900L);
WarehouseEntity warehouse = new WarehouseEntity(); warehouse.setId(900L); warehouse.setCustomerId(0L);
warehouse.setWarehouseCode("SHARED-LAX"); warehouse.setWarehouseName("Shared LAX");
warehouse.setWarehouseType("OVERSEAS"); warehouse.setStatus("ACTIVE");
ServiceCapabilityEntity receiving = new ServiceCapabilityEntity();
receiving.setCapabilityCode("RECEIVING"); receiving.setEnabled(true);
when(repository.findOwned(0L, 900L)).thenReturn(warehouse);
when(repository.findCapabilities(0L, 900L)).thenReturn(List.of(receiving));
assertThat(service.activeSharedReference(900L).warehouseName()).isEqualTo("Shared LAX");
}
@Test
void sharedWarehouseRejectsNonOverseasOrMissingReceivingCapability() {
assertThatThrownBy(() -> service.create(sharedRequest("FBA", true))).isInstanceOf(BizException.class);
assertThatThrownBy(() -> service.create(sharedRequest("OVERSEAS", false))).isInstanceOf(BizException.class);
}
private static WarehouseRequests.SaveRequest request() {
WarehouseRequests.AddressRequest address = new WarehouseRequests.AddressRequest(
"WAREHOUSE", "US", "CA", "Los Angeles", null, "90001",
@@ -67,4 +95,15 @@ class WarehouseApplicationServiceTest {
"OVERSEAS", "US", "America/Los_Angeles", null, null, null,
"ACTIVE", null, address, List.of());
}
private static WarehouseRequests.SaveRequest sharedRequest(String type, boolean receiving) {
WarehouseRequests.AddressRequest address = new WarehouseRequests.AddressRequest(
"WAREHOUSE", "US", "CA", "Los Angeles", null, "90001",
"1 Logistics Ave", null, null, null);
List<WarehouseRequests.CapabilityRequest> capabilities = receiving
? List.of(new WarehouseRequests.CapabilityRequest("RECEIVING", "收货", true, null, null))
: List.of();
return new WarehouseRequests.SaveRequest(0L, "SHARED-LAX", "Shared LAX", type,
"US", "America/Los_Angeles", null, null, null, "ACTIVE", null, address, capabilities);
}
}