fix: align phase5 overseas permissions

This commit is contained in:
2026-09-01 20:00:49 +08:00
parent a5d787b279
commit d88e5cffce
13 changed files with 459 additions and 55 deletions
@@ -5,10 +5,12 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.customervisit.dto.CustomerVisitRequests;
import com.baiyue.lastleg.customervisit.dto.CustomerVisitResponses;
import com.baiyue.lastleg.customervisit.service.CustomerVisitService;
import com.baiyue.lastleg.security.OverseasPermissions;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
@@ -29,12 +31,14 @@ public class CustomerVisitController {
@Resource private CustomerVisitService service;
@GetMapping("/customer-visits")
@PreAuthorize("hasAuthority('" + OverseasPermissions.CUSTOMER_VISIT_LIST + "')")
public ApiResponse<CustomerVisitResponses.Page> page(@AuthenticationPrincipal AuthenticatedUser user,
@Valid CustomerVisitRequests.Query query) {
return ApiResponse.success(service.page(scope(user), query));
}
@GetMapping("/customer-visits/{visitId}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.CUSTOMER_VISIT_LIST + "')")
public ApiResponse<CustomerVisitResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable long visitId) {
@@ -42,6 +46,7 @@ public class CustomerVisitController {
}
@PutMapping("/customer-visits/{visitId}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.CUSTOMER_VISIT_EDIT + "')")
public ApiResponse<CustomerVisitResponses.Detail> save(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable long visitId,
@@ -50,6 +55,7 @@ public class CustomerVisitController {
}
@PostMapping(value = "/customer-visits/{visitId}/attachments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasAuthority('" + OverseasPermissions.CUSTOMER_VISIT_EDIT + "')")
public ApiResponse<CustomerVisitResponses.UploadedFile> upload(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable long visitId,
@@ -59,6 +65,7 @@ public class CustomerVisitController {
}
@PostMapping("/delivery-orders/{deliveryNo}/complete")
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_STATUS + "')")
public ApiResponse<CustomerVisitResponses.Completed> completeDelivery(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@PathVariable String deliveryNo) {
@@ -66,6 +73,7 @@ public class CustomerVisitController {
}
@PutMapping("/delivery-orders/{deliveryNo}/status")
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_STATUS + "')")
public ApiResponse<CustomerVisitResponses.Completed> updateDeliveryStatus(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@PathVariable String deliveryNo, @Valid @RequestBody CustomerVisitRequests.DeliveryStatus request) {
@@ -29,7 +29,7 @@ public class DispatchQueryController {
@Resource private DispatchService service;
@GetMapping
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')")
@Operation(summary = "Page dispatch orders")
public ApiResponse<PageResult<DispatchResponse>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@@ -39,7 +39,7 @@ public class DispatchQueryController {
}
@GetMapping("/{dispatchNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')")
@Operation(summary = "Get dispatch details and all pallet destinations")
public ApiResponse<DispatchResponse> detail(
@AuthenticationPrincipal AuthenticatedUser user,
@@ -6,9 +6,11 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderRequests;
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderResponses;
import com.baiyue.lastleg.exceptionorder.service.ExceptionOrderService;
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.*;
import org.springframework.web.server.ResponseStatusException;
@@ -21,12 +23,14 @@ public class ExceptionOrderController {
@Resource private ExceptionOrderService service;
@GetMapping("/exception-orders")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_LIST + "')")
public ApiResponse<PageResult<ExceptionOrderResponses.Row>> page(@AuthenticationPrincipal AuthenticatedUser user,
@Valid ExceptionOrderRequests.Query query) {
return ApiResponse.success(service.page(scope(user), query));
}
@GetMapping("/exception-orders/resolve")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_LIST + "')")
public ApiResponse<ExceptionOrderResponses.Relation> resolve(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@RequestParam(required = false) String inboundCode,
@@ -35,12 +39,14 @@ public class ExceptionOrderController {
}
@PostMapping("/exception-orders")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_CREATE + "')")
public ApiResponse<ExceptionOrderResponses.Created> create(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ExceptionOrderRequests.Create request) {
return ApiResponse.success(service.create(scope(user), operator(user), request));
}
@GetMapping("/delivery-orders/{deliveryNo}/return-inbound-options")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_LIST + "')")
public ApiResponse<List<ExceptionOrderResponses.Relation>> returnOptions(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable String deliveryNo) {
@@ -48,6 +54,7 @@ public class ExceptionOrderController {
}
@PostMapping("/delivery-orders/{deliveryNo}/mark-return")
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_RETURN + "')")
public ApiResponse<ExceptionOrderResponses.Created> markReturn(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable String deliveryNo,
@@ -20,6 +20,9 @@ public final class OverseasPermissions {
public static final String INVENTORY_EDIT = "platform:overseas-warehouse:inventory:edit";
public static final String INVENTORY_ADJUST = "platform:overseas-warehouse:inventory:adjust";
public static final String DELIVERY_LIST = "platform:overseas-warehouse:delivery:list";
public static final String DELIVERY_RETURN = "platform:overseas-warehouse:delivery:return";
public static final String DELIVERY_STATUS = "platform:overseas-warehouse:delivery:status";
public static final String DISPATCH_LIST = "platform:overseas-warehouse:dispatch:list";
public static final String DISPATCH_EDIT = "platform:overseas-warehouse:dispatch:edit";
public static final String DISPATCH_CANCEL = "platform:overseas-warehouse:dispatch:cancel";
public static final String DISPATCH_TRANSFER_DELIVERY =
@@ -27,6 +30,10 @@ public final class OverseasPermissions {
public static final String OUTBOUND_LIST = "platform:overseas-warehouse:outbound:list";
public static final String OUTBOUND_CREATE = "platform:overseas-warehouse:outbound:create";
public static final String OUTBOUND_CANCEL = "platform:overseas-warehouse:outbound:cancel";
public static final String EXCEPTION_ORDER_LIST = "platform:overseas-warehouse:exception-order:list";
public static final String EXCEPTION_ORDER_CREATE = "platform:overseas-warehouse:exception-order:create";
public static final String CUSTOMER_VISIT_LIST = "platform:overseas-warehouse:customer-visit:list";
public static final String CUSTOMER_VISIT_EDIT = "platform:overseas-warehouse:customer-visit:edit";
public static final String MERCHANT_DELIVERY_LIST = "merchant:warehouse:delivery:list";
public static final String MERCHANT_DELIVERY_CREATE = "merchant:warehouse:delivery:create";
public static final String PURCHASE_LIST = "platform:overseas-business:purchase:list";
@@ -365,7 +365,7 @@ paths:
/api/platform/v1/last-leg/dispatches:
get:
summary: 分页查询排车单
description: 权限 platform:overseas-warehouse:inbound-order:list;平台跨客户分页查询,前端不传 customerId。
description: 权限 platform:overseas-warehouse:dispatch:list;平台跨客户分页查询,前端不传 customerId。
security: [{ bearerAuth: [] }]
parameters:
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
@@ -379,7 +379,7 @@ paths:
/api/platform/v1/last-leg/dispatches/{dispatchNo}:
get:
summary: 查询排车单详情
description: 权限 platform:overseas-warehouse:inbound-order:list;平台按全局排车单号定位记录,并使用记录自身客户范围查询明细,前端不传 customerId。
description: 权限 platform:overseas-warehouse:dispatch:list;列表权限包含详情只读;平台按全局排车单号定位记录,并使用记录自身客户范围查询明细,前端不传 customerId。
security: [{ bearerAuth: [] }]
parameters:
- { name: dispatchNo, in: path, required: true, schema: { type: string } }
@@ -523,6 +523,95 @@ paths:
parameters: [{ name: outboundNo, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/OutboundCancel' } } } }
responses: { '200': { description: 取消成功或已取消 }, '404': { description: 出库单不存在 }, '409': { description: 出库已开始执行 } }
/api/platform/v1/last-leg/delivery-orders:
get:
summary: 分页查询平台派送单
description: 权限 platform:overseas-warehouse:delivery:list;customerId 仅为可选平台筛选条件。
security: [{ bearerAuth: [] }]
responses: { '200': { description: 派送单分页 } }
/api/platform/v1/last-leg/delivery-orders/{deliveryNo}:
get:
summary: 查询平台派送单详情
description: 权限 platform:overseas-warehouse:delivery:list;列表权限包含详情只读。
security: [{ bearerAuth: [] }]
parameters: [{ name: deliveryNo, in: path, required: true, schema: { type: string } }]
responses: { '200': { description: 派送单详情 }, '404': { description: 派送单不存在 } }
/api/platform/v1/last-leg/exception-orders:
get:
summary: 分页查询异常订单
description: 权限 platform:overseas-warehouse:exception-order:list。
security: [{ bearerAuth: [] }]
responses: { '200': { description: 异常订单分页 } }
post:
summary: 创建异常订单
description: 权限 platform:overseas-warehouse:exception-order:create。
security: [{ bearerAuth: [] }]
requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
responses: { '200': { description: 已创建异常订单 } }
/api/platform/v1/last-leg/exception-orders/resolve:
get:
summary: 解析异常订单关联关系
description: 权限 platform:overseas-warehouse:exception-order:list;属于列表页只读辅助接口。
security: [{ bearerAuth: [] }]
responses: { '200': { description: 关联关系 } }
/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/return-inbound-options:
get:
summary: 查询派送退回候选入仓单
description: 权限 platform:overseas-warehouse:exception-order:list;属于异常订单列表只读辅助接口。
security: [{ bearerAuth: [] }]
parameters: [{ name: deliveryNo, in: path, required: true, schema: { type: string } }]
responses: { '200': { description: 退回候选入仓单 } }
/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/mark-return:
post:
summary: 将派送单标记为退回入仓
description: 权限 platform:overseas-warehouse:delivery:return。
security: [{ bearerAuth: [] }]
parameters: [{ name: deliveryNo, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
responses: { '200': { description: 已创建退回异常单 } }
/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/complete:
post:
summary: 标记派送完成并生成回访记录
description: 权限 platform:overseas-warehouse:delivery:status。
security: [{ bearerAuth: [] }]
parameters: [{ name: deliveryNo, in: path, required: true, schema: { type: string } }]
responses: { '200': { description: 派送状态与回访记录 } }
/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/status:
put:
summary: 更新派送状态
description: 权限 platform:overseas-warehouse:delivery:status。
security: [{ bearerAuth: [] }]
parameters: [{ name: deliveryNo, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
responses: { '200': { description: 更新后的派送状态 } }
/api/platform/v1/last-leg/customer-visits:
get:
summary: 分页查询客户回访
description: 权限 platform:overseas-warehouse:customer-visit:list。
security: [{ bearerAuth: [] }]
responses: { '200': { description: 客户回访分页 } }
/api/platform/v1/last-leg/customer-visits/{visitId}:
get:
summary: 查询客户回访详情
description: 权限 platform:overseas-warehouse:customer-visit:list;列表权限包含详情只读。
security: [{ bearerAuth: [] }]
parameters: [{ name: visitId, in: path, required: true, schema: { type: integer, format: int64 } }]
responses: { '200': { description: 客户回访详情 } }
put:
summary: 保存客户回访
description: 权限 platform:overseas-warehouse:customer-visit:edit。
security: [{ bearerAuth: [] }]
parameters: [{ name: visitId, in: path, required: true, schema: { type: integer, format: int64 } }]
requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
responses: { '200': { description: 已保存客户回访 } }
/api/platform/v1/last-leg/customer-visits/{visitId}/attachments:
post:
summary: 上传客户回访附件
description: 权限 platform:overseas-warehouse:customer-visit:edit。
security: [{ bearerAuth: [] }]
parameters: [{ name: visitId, in: path, required: true, schema: { type: integer, format: int64 } }]
requestBody: { required: true, content: { multipart/form-data: { schema: { type: object } } } }
responses: { '200': { description: 已上传附件 } }
/api/platform/v1/last-leg/overseas-procurements:
get:
summary: 分页查询海外采购订单
@@ -50,7 +50,7 @@ class DispatchQueryControllerTest {
}
@Test
void listAndDetailReuseInboundOrderListAuthority() throws Exception {
void listAndDetailUseDedicatedDispatchListAuthority() throws Exception {
PreAuthorize page = DispatchQueryController.class
.getMethod("page", AuthenticatedUser.class, DispatchRequests.Query.class)
.getAnnotation(PreAuthorize.class);
@@ -58,8 +58,8 @@ class DispatchQueryControllerTest {
.getMethod("detail", AuthenticatedUser.class, String.class)
.getAnnotation(PreAuthorize.class);
assertThat(page.value()).contains(OverseasPermissions.INBOUND_ORDER_LIST);
assertThat(detail.value()).contains(OverseasPermissions.INBOUND_ORDER_LIST);
assertThat(page.value()).isEqualTo("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')");
assertThat(detail.value()).isEqualTo("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')");
}
private static AuthenticatedUser platform() {
@@ -0,0 +1,81 @@
package com.baiyue.lastleg.security;
import com.baiyue.lastleg.customervisit.controller.CustomerVisitController;
import com.baiyue.lastleg.exceptionorder.controller.ExceptionOrderController;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class Phase5AdminPermissionContractTest {
@Test
void exceptionAndDeliveryReturnEndpointsUseIndependentAuthorities() {
Map<String, String> permissions = permissions(ExceptionOrderController.class);
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("page"));
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("resolve"));
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_CREATE), permissions.get("create"));
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("returnOptions"));
assertEquals(authority(OverseasPermissions.DELIVERY_RETURN), permissions.get("markReturn"));
}
@Test
void customerVisitAndDeliveryStatusEndpointsUseIndependentAuthorities() {
Map<String, String> permissions = permissions(CustomerVisitController.class);
assertEquals(authority(OverseasPermissions.CUSTOMER_VISIT_LIST), permissions.get("page"));
assertEquals(authority(OverseasPermissions.CUSTOMER_VISIT_LIST), permissions.get("detail"));
assertEquals(authority(OverseasPermissions.CUSTOMER_VISIT_EDIT), permissions.get("save"));
assertEquals(authority(OverseasPermissions.CUSTOMER_VISIT_EDIT), permissions.get("upload"));
assertEquals(authority(OverseasPermissions.DELIVERY_STATUS), permissions.get("completeDelivery"));
assertEquals(authority(OverseasPermissions.DELIVERY_STATUS), permissions.get("updateDeliveryStatus"));
}
@Test
void openApiPublishesEveryFrozenPhase5AdminAuthorityAndPath() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/openapi/overseas-business-v1.yaml"),
StandardCharsets.UTF_8);
for (String permission : new String[] {
OverseasPermissions.DISPATCH_LIST,
OverseasPermissions.EXCEPTION_ORDER_LIST,
OverseasPermissions.EXCEPTION_ORDER_CREATE,
OverseasPermissions.DELIVERY_RETURN,
OverseasPermissions.DELIVERY_STATUS,
OverseasPermissions.CUSTOMER_VISIT_LIST,
OverseasPermissions.CUSTOMER_VISIT_EDIT
}) {
assertTrue(yaml.contains(permission), permission);
}
for (String path : new String[] {
"/api/platform/v1/last-leg/dispatches:",
"/api/platform/v1/last-leg/exception-orders:",
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/mark-return:",
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/status:",
"/api/platform/v1/last-leg/customer-visits:",
"/api/platform/v1/last-leg/customer-visits/{visitId}:"
}) {
assertTrue(yaml.contains(path), path);
}
}
private static Map<String, String> permissions(Class<?> controller) {
return Arrays.stream(controller.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(PreAuthorize.class))
.collect(Collectors.toMap(Method::getName,
method -> method.getAnnotation(PreAuthorize.class).value()));
}
private static String authority(String permission) {
return "hasAuthority('" + permission + "')";
}
}