diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CertificationAuditController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CertificationAuditController.java index efcdae5d..d4528ea1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CertificationAuditController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CertificationAuditController.java @@ -37,6 +37,7 @@ import org.springblade.core.tool.api.R; import org.springblade.transport.pojo.vo.CertAuditDetailVO; import org.springblade.transport.pojo.vo.TransportCertRecordVO; import org.springblade.transport.service.ICertAuditService; +import org.springblade.transport.support.CertAuditPermissions; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -56,31 +57,12 @@ import org.springframework.web.bind.annotation.RestController; @Tag(name = "认证审核", description = "管理端司机认证与车辆认证审核") public class CertificationAuditController extends BladeController { - /** - * 待审列表与详情的权限:司机或车辆任一审核菜单即可。 - *
- * 用菜单码而不是角色名——本项目的权限走「角色-菜单」授权, - * 写死角色名会漏掉实际承担审核的岗位(例如「运管」), - * 也会在角色改名后静默失效。 - *
- * 用 SpEL 而非 {@code @PreAuth(menu = "...")},是因为同一组接口同时服务
- * 司机与车辆两侧(由 {@code bizType} 区分),单值 menu 表达不了「二者之一」。
- */
- private static final String AUDIT_MENU_EXPRESSION =
- "hasMenu('cert_certification_audit_driver') or hasMenu('cert_certification_audit_vehicle')";
-
- /**
- * 审核动作的权限:司机或车辆任一「审核」按钮权限即可
- */
- private static final String AUDIT_ACTION_MENU_EXPRESSION =
- "hasMenu('cert_certification_audit_driver_audit') or hasMenu('cert_certification_audit_vehicle_audit')";
-
private final ICertAuditService certAuditService;
@GetMapping("/pending-page")
@ApiOperationSupport(order = 1)
@Operation(summary = "待审列表", description = "按认证对象类型分页查询审核中的记录,按提交时间倒序")
- @PreAuth(AUDIT_MENU_EXPRESSION)
+ @PreAuth(CertAuditPermissions.AUDIT_VIEW)
public R
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support;
+
+/**
+ * 认证审核的权限表达式
+ *
+ * 用菜单码而不是角色名。本项目的权限走「角色-菜单」授权,写死角色名有两个坑:
+ * 角色别名可能对不上(曾写成 {@code admin},而库里只有 {@code administrator}),
+ * 以及会漏掉实际承担审核的岗位(例如「运管」)。
+ *
+ * 用表达式而不是 {@code @PreAuth(menu = "...")} 的单值形式,是因为同一组接口
+ * 由 {@code bizType} 参数区分、同时服务司机与车辆两侧,单值 menu 表达不了「二者之一」。
+ *
+ * 表达式最终由框架交给 SpEL 求值,root object 是 {@code AuthFun},
+ * 故这里用的函数名必须与它的公开方法一致({@code hasMenu})。
+ * {@code CertAuditPermissionsTest} 会实际解析并求值这两个表达式,
+ * 一旦写错函数名或括号,测试即失败——而不是等到线上请求时抛 500。
+ *
+ * @author Chill
+ */
+public final class CertAuditPermissions {
+
+ /**
+ * 待审列表与审核详情:司机或车辆任一审核菜单即可
+ */
+ public static final String AUDIT_VIEW =
+ "hasMenu('cert_certification_audit_driver') or hasMenu('cert_certification_audit_vehicle')";
+
+ /**
+ * 审核动作:司机或车辆任一「审核」按钮权限即可
+ */
+ public static final String AUDIT_ACTION =
+ "hasMenu('cert_certification_audit_driver_audit') or hasMenu('cert_certification_audit_vehicle_audit')";
+
+ private CertAuditPermissions() {
+ }
+
+}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertAuditPermissionsTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertAuditPermissionsTest.java
new file mode 100644
index 00000000..4422d9cd
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertAuditPermissionsTest.java
@@ -0,0 +1,99 @@
+package org.springblade.transport.support;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springblade.core.secure.auth.AuthFun;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * 认证审核权限表达式的可求值性测试。
+ *
+ * 这些字符串写在 {@code @PreAuth} 注解里,由框架在**请求时**交给 SpEL 求值,
+ * root object 是 {@code AuthFun}。名字写错不会在编译期或启动期暴露,
+ * 只会在真实调用时抛异常(表现为接口 500)——用测试把它提前到构建期。
+ *
+ * 这里用同一个 {@link SpelExpressionParser} 与同一套 root object 语义求值,
+ * 与框架 {@code AuthAspect} 的做法一致。
+ */
+class CertAuditPermissionsTest {
+
+ private final SpelExpressionParser parser = new SpelExpressionParser();
+
+ @Test
+ @DisplayName("查看权限:拥有司机审核菜单即可通过")
+ void auditViewWithDriverMenu() {
+ AuthFun authFun = mock(AuthFun.class);
+ when(authFun.hasMenu("cert_certification_audit_driver")).thenReturn(true);
+
+ assertTrue(evaluate(CertAuditPermissions.AUDIT_VIEW, authFun),
+ "只授予司机审核菜单的用户也应能查看待审列表");
+ }
+
+ @Test
+ @DisplayName("查看权限:拥有车辆审核菜单即可通过")
+ void auditViewWithVehicleMenu() {
+ AuthFun authFun = mock(AuthFun.class);
+ when(authFun.hasMenu("cert_certification_audit_vehicle")).thenReturn(true);
+
+ assertTrue(evaluate(CertAuditPermissions.AUDIT_VIEW, authFun));
+ }
+
+ @Test
+ @DisplayName("查看权限:两个菜单都没有则拒绝")
+ void auditViewWithoutAnyMenu() {
+ assertFalse(evaluate(CertAuditPermissions.AUDIT_VIEW, mock(AuthFun.class)),
+ "无任何审核菜单的用户不得查看待审列表");
+ }
+
+ @Test
+ @DisplayName("审核动作:只有查看权限、没有审核按钮权限时必须拒绝")
+ void auditActionRequiresButtonPermission() {
+ AuthFun authFun = mock(AuthFun.class);
+ // 只给了列表菜单,没给「审核」按钮
+ when(authFun.hasMenu("cert_certification_audit_driver")).thenReturn(true);
+
+ assertFalse(evaluate(CertAuditPermissions.AUDIT_ACTION, authFun),
+ "能看待审列表不等于能执行审核");
+ }
+
+ @Test
+ @DisplayName("审核动作:任一审核按钮权限即可通过")
+ void auditActionWithEitherButton() {
+ AuthFun driverAuditor = mock(AuthFun.class);
+ when(driverAuditor.hasMenu("cert_certification_audit_driver_audit")).thenReturn(true);
+ assertTrue(evaluate(CertAuditPermissions.AUDIT_ACTION, driverAuditor));
+
+ AuthFun vehicleAuditor = mock(AuthFun.class);
+ when(vehicleAuditor.hasMenu("cert_certification_audit_vehicle_audit")).thenReturn(true);
+ assertTrue(evaluate(CertAuditPermissions.AUDIT_ACTION, vehicleAuditor));
+ }
+
+ @Test
+ @DisplayName("表达式必须引用真实存在的菜单码(与票 01 建的一致)")
+ void expressionsReferenceRealMenuCodes() {
+ assertTrue(CertAuditPermissions.AUDIT_VIEW.contains("cert_certification_audit_driver"));
+ assertTrue(CertAuditPermissions.AUDIT_VIEW.contains("cert_certification_audit_vehicle"));
+ assertTrue(CertAuditPermissions.AUDIT_ACTION.contains("cert_certification_audit_driver_audit"));
+ assertTrue(CertAuditPermissions.AUDIT_ACTION.contains("cert_certification_audit_vehicle_audit"));
+ // 不应再出现角色名判断:库里不存在 admin 角色,且会漏掉「运管」岗位
+ assertFalse(CertAuditPermissions.AUDIT_VIEW.contains("hasAnyRole"));
+ assertFalse(CertAuditPermissions.AUDIT_ACTION.contains("hasAnyRole"));
+ }
+
+ /**
+ * 用与框架相同的方式求值:AuthFun 作为 root object
+ */
+ private boolean evaluate(String expression, AuthFun authFun) {
+ Expression parsed = parser.parseExpression(expression);
+ StandardEvaluationContext context = new StandardEvaluationContext(authFun);
+ return Boolean.TRUE.equals(parsed.getValue(context, Boolean.class));
+ }
+
+}