From a563f6385714cd2839751f9f4b7cb3a7b24c9d58 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 19 Oct 2026 17:02:25 +0800 Subject: [PATCH] feat(appointments): expose destination warehouse options --- .../AppointmentCargoService.java | 5 +++++ .../AppointmentPlanningController.java | 8 ++++++++ .../openapi/appointment-planning-v1.yaml | 20 +++++++++++++++++++ .../AppointmentCargoServiceTest.java | 15 ++++++++++++++ .../AppointmentPlanningAuthorizationTest.java | 14 +++++++++++++ 5 files changed, 62 insertions(+) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoService.java index 507af0b90..08a784e18 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoService.java @@ -35,6 +35,11 @@ public class AppointmentCargoService { return new PageResult<>(rows, page, size, total, (total + size - 1) / size); } + /** 查询下拉框与货物分页共用权威目录范围,不从当前页猜测目的仓。 */ + public List destinations() { + return scope().destinations().stream().distinct().sorted().toList(); + } + private record Scope(List origins, List destinations) { } private Scope scope() { final List catalog; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningController.java index 752a0c99f..2d78bfbcf 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningController.java @@ -53,6 +53,14 @@ public class AppointmentPlanningController { return ApiResponse.success(new Resolved(source,result.get(0))); } + /** 目的仓选项沿用货物查询权限,只返回同范围的美国FBA仓码。 */ + @GetMapping("/cargo/destinations") + @PreAuthorize("hasAuthority('platform:appointment:list') and hasAuthority('platform:overseas-warehouse:last-mile-order:list')") + public ApiResponse> destinations(@AuthenticationPrincipal AuthenticatedUser user) { + requirePlatform(user); + return ApiResponse.success(service.destinations()); + } + @GetMapping("/cargo") @PreAuthorize("hasAuthority('platform:appointment:list') and hasAuthority('platform:overseas-warehouse:last-mile-order:list')") public ApiResponse> cargo(@AuthenticationPrincipal AuthenticatedUser user, diff --git a/lastleg-service/src/main/resources/openapi/appointment-planning-v1.yaml b/lastleg-service/src/main/resources/openapi/appointment-planning-v1.yaml index cae7341c6..9ea4eb474 100644 --- a/lastleg-service/src/main/resources/openapi/appointment-planning-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/appointment-planning-v1.yaml @@ -6,6 +6,26 @@ info: security: - bearerAuth: [] paths: + /api/platform/v1/last-leg/appointment-planning/cargo/destinations: + get: + operationId: listAppointmentCargoDestinations + summary: 读取预约货物查询的目的FBA仓选项 + description: >- + 仅平台;同时要求platform:appointment:list与platform:overseas-warehouse:last-mile-order:list。 + 与货物分页共用权威启用美国FBA目录,返回去重、按仓码排序的完整选项,不受货物当前分页或关键词限制。 + 不增加仓库主档读取权限;目录失败返回503,不当作空选项。 + responses: + '200': + description: 业务响应;data为目的FBA仓码数组 + content: + application/json: + schema: + type: object + properties: + code: {type: string} + data: {type: array, items: {type: string}, example: [ABE4, ONT8]} + '403': {description: 非平台或缺少任一读取权限} + '503': {description: 权威目录不可用} /api/platform/v1/last-leg/appointment-planning/sources/resolve-order: get: operationId: resolveOrderAppointmentSource diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoServiceTest.java index 280e1dc8a..820e9359a 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoServiceTest.java @@ -8,6 +8,21 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class AppointmentCargoServiceTest { + /** 选项取完整权威美国FBA目录,不依赖当前页货物或新增仓库主档权限。 */ + @Test void destinationOptionsUseTheSameAmericanFbaScopeWithoutQueryingCargoRows() { + var mapper = mock(AppointmentCargoMapper.class); + var warehouses = mock(AppointmentWarehouseClient.class); + when(warehouses.catalog("token")).thenReturn(ApiResponse.success(List.of( + new AppointmentWarehouseClient.Warehouse(1L,"US-WH","美国仓","OVERSEAS","US"), + new AppointmentWarehouseClient.Warehouse(2L,"ONT8","ONT8","FBA","US"), + new AppointmentWarehouseClient.Warehouse(3L,"ABE4","ABE4","FBA","US"), + new AppointmentWarehouseClient.Warehouse(4L,"YYZ1","YYZ1","FBA","CA"), + new AppointmentWarehouseClient.Warehouse(5L,"ONT8","ONT8","FBA","US")))); + assertEquals(List.of("ABE4","ONT8"),new AppointmentCargoService(mapper,warehouses,"token").destinations()); + verifyNoInteractions(mapper); + when(warehouses.catalog("token")).thenThrow(new IllegalStateException("offline")); + assertEquals(503,assertThrows(ResponseStatusException.class,()->new AppointmentCargoService(mapper,warehouses,"token").destinations()).getStatusCode().value()); + } @Test void authenticUsDirectoryScopesTheDatabaseQueryAndPreservesServerPagination() { var mapper = mock(AppointmentCargoMapper.class); var warehouses = mock(AppointmentWarehouseClient.class); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningAuthorizationTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningAuthorizationTest.java index 13e21e691..da1ac5ea4 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningAuthorizationTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningAuthorizationTest.java @@ -17,6 +17,20 @@ import static org.mockito.Mockito.*; class AppointmentPlanningAuthorizationTest { private static final String LIST="platform:appointment:list", CARGO="platform:overseas-warehouse:last-mile-order:list", WRITE="platform:appointment:followup"; + /** 目的仓选项不要求仓库管理权限,但保留原两项读取权限和平台身份边界。 */ + @Test void destinationOptionsRequireTheExistingCargoReadPermissionsAndPlatformIdentity() { + try(var context=new AnnotationConfigApplicationContext(Config.class)) { + var controller=context.getBean(AppointmentPlanningController.class); + var service=context.getBean(AppointmentCargoService.class); + login(LIST);assertThrows(AccessDeniedException.class,()->controller.destinations(user(0L,"PLATFORM"))); + login(CARGO);assertThrows(AccessDeniedException.class,()->controller.destinations(user(0L,"PLATFORM"))); + login(LIST,CARGO); + assertThrows(ResponseStatusException.class,()->controller.destinations(user(1L,"CLIENT"))); + verifyNoInteractions(service); + when(service.destinations()).thenReturn(List.of("ABE4","ONT8")); + assertEquals(List.of("ABE4","ONT8"),controller.destinations(user(0L,"PLATFORM")).data()); + } finally { SecurityContextHolder.clearContext(); } + } @Test void orderLookupUsesCanonicalBookingSourceAndEnforcesCustomerAndReadPermissions() { try(var context=new AnnotationConfigApplicationContext(Config.class)) { var controller=context.getBean(AppointmentPlanningController.class);