diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java index c4cd9a2bb..e8ab8e2ff 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java @@ -46,7 +46,7 @@ public class DeliveryTransferController { @PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_CREATE + "')") @Operation(summary = "List server-authoritative one-click dropship outbound candidates") public ApiResponse> oneClickCandidates( - @AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId, + @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @RequestParam(required = false) String warehouseId) { requirePlatform(user); return ApiResponse.success(service.oneClickCandidates(customerId, warehouseId)); diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 3c9c54383..7bb8f3de0 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -665,10 +665,10 @@ paths: /api/platform/v1/last-leg/outbound-orders/one-click-dropship/candidates: get: summary: 查询一件代发出库候选库存 - description: 权限 platform:overseas-warehouse:outbound:create;按客户及可选海外仓一次查询服务端权威候选,仅返回 ACTIVE 一件代发档案、OCCUPIED 正可用库存、AVAILABLE 箱唛与 SKU 余额共同覆盖且未被排车、转仓或待执行出库占用的维度。创建命令会再次校验,不信任前端候选快照。 + description: 权限 platform:overseas-warehouse:outbound:create;仅平台身份可查询,customerId 为可选客户筛选条件,未传时跨客户查询,可按海外仓筛选。仅返回 ACTIVE 一件代发档案、OCCUPIED 正可用库存、AVAILABLE 箱唛与 SKU 余额共同覆盖且未被排车、转仓或待执行出库占用的维度。创建命令仍必须指定唯一客户并再次校验客户与仓库,不信任前端候选快照。 security: [{ bearerAuth: [] }] parameters: - - { name: customerId, in: query, required: true, schema: { type: string, pattern: '^[1-9][0-9]*$' } } + - { name: customerId, in: query, required: false, schema: { type: string, pattern: '^[1-9][0-9]*$' } } - { name: warehouseId, in: query, required: false, schema: { type: string } } responses: '200': diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DropshipOutboundCandidateMapperContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DropshipOutboundCandidateMapperContractTest.java index bc3bb4a84..df43dfc50 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DropshipOutboundCandidateMapperContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DropshipOutboundCandidateMapperContractTest.java @@ -19,6 +19,7 @@ class DropshipOutboundCandidateMapperContractTest { assertTrue(sql.contains(token), token); } assertTrue(sql.contains("LEAST(pos.available_packages,s.package_count")); + assertTrue(sql.contains("#{customerId} IS NULL OR #{customerId}='' OR p.customer_id=#{customerId}")); } @Test diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java index 0f5679676..2aa4a5763 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java @@ -36,6 +36,37 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class PlatformDispatchOutboundContractTest { + @Test + void candidateHttpQueryAcceptsWarehouseWithoutCustomerAndPreservesExplicitFilter() throws Exception { + DeliveryTransferController controller = new DeliveryTransferController(); + var service = mock(com.baiyue.lastleg.delivery.service.DeliveryTransferService.class); + ReflectionTestUtils.setField(controller, "service", service); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller) + .setCustomArgumentResolvers(new AuthenticationPrincipalArgumentResolver()).build(); + AuthenticatedUser platform = new AuthenticatedUser(1000001L, 0L, 0L, 1L, "platform_admin", + "PLATFORM", "WEB", Set.of(OverseasPermissions.OUTBOUND_CREATE)); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(platform, null, Set.of())); + String path = "/api/platform/v1/last-leg/outbound-orders/one-click-dropship/candidates"; + try { + mvc.perform(get(path).param("warehouseId", "WH-1")).andExpect(status().isOk()); + org.mockito.Mockito.verify(service).oneClickCandidates(null, "WH-1"); + mvc.perform(get(path).param("customerId", "101").param("warehouseId", "WH-1")) + .andExpect(status().isOk()); + org.mockito.Mockito.verify(service).oneClickCandidates("101", "WH-1"); + SecurityContextHolder.clearContext(); + mvc.perform(get(path).param("warehouseId", "WH-1")).andExpect(status().isUnauthorized()); + AuthenticatedUser merchant = new AuthenticatedUser(7L, 100L, 100L, 8L, "merchant", + "MERCHANT", "WEB", Set.of(OverseasPermissions.OUTBOUND_CREATE)); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(merchant, null, Set.of())); + mvc.perform(get(path).param("warehouseId", "WH-1")).andExpect(status().isForbidden()); + org.mockito.Mockito.verifyNoMoreInteractions(service); + } finally { + SecurityContextHolder.clearContext(); + } + } + @Test void outboundDetailAcceptsTheDocumentUrlWithoutPagingParameters() throws Exception { DeliveryTransferController controller = new DeliveryTransferController(); @@ -87,7 +118,7 @@ class PlatformDispatchOutboundContractTest { } @Test - void oneClickRequiresCustomerWhileOutboundListKeepsOptionalFilter() throws Exception { + void oneClickCreationRequiresCustomerWhileCandidateAndListFiltersAreOptional() throws Exception { Method create = DeliveryTransferController.class.getMethod("createOneClick", AuthenticatedUser.class, String.class, DeliveryRequests.OneClickOutbound.class); Method candidates = DeliveryTransferController.class.getMethod("oneClickCandidates", AuthenticatedUser.class, @@ -102,7 +133,7 @@ class PlatformDispatchOutboundContractTest { RequestParam pageCustomer = page.getParameters()[1].getAnnotation(RequestParam.class); assertNotNull(createCustomer); assertTrue(createCustomer.required()); - assertTrue(candidates.getParameters()[1].getAnnotation(RequestParam.class).required()); + assertFalse(candidates.getParameters()[1].getAnnotation(RequestParam.class).required()); assertFalse(candidates.getParameters()[2].getAnnotation(RequestParam.class).required()); assertNotNull(pageCustomer); assertFalse(pageCustomer.required()); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java index 78a6a9895..87d248ed6 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java @@ -557,6 +557,20 @@ class DeliveryTransferServiceTest { verify(inventoryPositions, never()).selectById(any()); } + @Test + void oneClickCandidatesAllowPlatformWarehouseQueryWithoutCustomer() { + DropshipOutboundCandidate first = dropshipCandidate(901L, 1001L, "WH-1", "DS-A001", "SKU-1", 1L); + DropshipOutboundCandidate second = dropshipCandidate(902L, 1002L, "WH-1", "DS-A002", "SKU-1", 3L); + when(dropshipProfiles.listOutboundCandidates(null, "WH-1")).thenReturn(List.of(first, second)); + + var result = service.oneClickCandidates(null, " WH-1 "); + + assertEquals(2, result.size()); + assertEquals("DS-A001", result.get(0).palletNo()); + assertEquals("DS-A002", result.get(1).palletNo()); + verify(dropshipProfiles).listOutboundCandidates(null, "WH-1"); + } + @Test void oneClickCreationRejectsOrdinaryInventoryBeforeAnyWrite() {