diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditCapabilityVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditCapabilityVO.java new file mode 100644 index 00000000..7b71e537 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditCapabilityVO.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * 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.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 认证审核能力 + *

+ * 无权限时两个字段为 false;本接口不挂权限表达式。 + * 列表与详情各自的权限表达式不变。 + * + * @author Chill + */ +@Data +@Schema(description = "认证审核能力") +public class CertAuditCapabilityVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "能否查看审核列表与详情:无权限时为 false,不抛 403") + private Boolean canView; + + @Schema(description = "能否执行审核(通过/驳回):无权限时为 false,不抛 403") + private Boolean canAudit; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditDetailVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditDetailVO.java index 577ce62e..212eb072 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditDetailVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditDetailVO.java @@ -76,6 +76,8 @@ public class CertAuditDetailVO implements Serializable { private String auditUserName; @Schema(description = "审核时间(通过或驳回的时间);未审核时为 null") private LocalDateTime auditTime; + @Schema(description = "驳回原因;通过或未审核时为空") + private String rejectReason; @Schema(description = "待审快照:本次提交的全量内容") private String pendingSnapshotJson; 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 a7b8f544..6e6a024f 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 @@ -36,6 +36,7 @@ import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.vo.CertAuditCapabilityVO; import org.springblade.transport.pojo.vo.CertAuditDetailVO; import org.springblade.transport.pojo.vo.TransportCertRecordVO; import org.springblade.transport.service.ICertAuditService; @@ -100,4 +101,12 @@ public class CertificationAuditController extends BladeController { return R.status(certAuditService.audit(id, Boolean.TRUE.equals(pass), rejectReason)); } + // 本接口不挂 @PreAuth;无权限时返回 false。 + @GetMapping("/capability") + @ApiOperationSupport(order = 4) + @Operation(summary = "审核能力判定", description = "无权限返回 false,不抛 403,供小程序决定空态与按钮禁用") + public R capability() { + return R.data(certAuditService.capability()); + } + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICertAuditService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICertAuditService.java index 89af4d43..bdfd3327 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICertAuditService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICertAuditService.java @@ -27,6 +27,7 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.transport.pojo.entity.TransportCertRecord; +import org.springblade.transport.pojo.vo.CertAuditCapabilityVO; import org.springblade.transport.pojo.vo.CertAuditDetailVO; import org.springblade.transport.pojo.vo.TransportCertRecordVO; @@ -80,4 +81,16 @@ public interface ICertAuditService { */ boolean audit(Long recordId, boolean pass, String rejectReason); + /** + * 当前登录用户的审核能力:能否查看、能否审核 + *

+ * 小程序端据此决定无权限空态与审核按钮禁用。两项都按菜单码判断, + * 没有权限时返回 false,**不抛异常**;未登录仍由全局鉴权链拦截,与其它接口一致。 + *

+ * 只读,无副作用;不带租户或对象参数,答案与具体记录无关。 + * + * @return 审核能力,两个字段均不为 null + */ + CertAuditCapabilityVO capability(); + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java index 14fa3cbf..a35ee3a3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java @@ -30,6 +30,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.secure.handler.IPermissionHandler; import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.UserCache; @@ -40,13 +41,16 @@ import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest; import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.pojo.entity.TransportCertRecord; import org.springblade.transport.pojo.entity.TransportVehicle; +import org.springblade.transport.pojo.vo.CertAuditCapabilityVO; import org.springblade.transport.pojo.vo.CertAuditDetailVO; import org.springblade.transport.pojo.vo.TransportCertRecordVO; import org.springblade.transport.service.ICertAuditService; import org.springblade.transport.service.IDriverService; import org.springblade.transport.service.ITransportCertRecordService; import org.springblade.transport.service.ITransportVehicleService; +import org.springblade.transport.support.CertAuditPermissions; import org.springblade.transport.support.CertificationRuleSupport; +import org.springblade.transport.support.MpUserDataScopeSupport; import org.springblade.transport.wrapper.TransportCertRecordWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -75,6 +79,8 @@ public class CertAuditServiceImpl implements ICertAuditService { private final TransportCertRecordMapper certRecordMapper; private final ITransportVehicleService transportVehicleService; private final IDriverService driverService; + private final IPermissionHandler permissionHandler; + private final MpUserDataScopeSupport mpUserDataScopeSupport; @Override public IPage pendingPage(String bizType, String bizKey, String driverName, String driverMobile, @@ -130,6 +136,7 @@ public class CertAuditServiceImpl implements ICertAuditService { detail.setAuditUser(record.getAuditUser()); detail.setAuditTime(record.getAuditTime()); detail.setAuditUserName(UserCache.getUserRealName(record.getAuditUser())); + detail.setRejectReason(record.getRejectReason()); detail.setPendingSnapshotJson(record.getSnapshotJson()); // 生效值一侧:从未认证通过时为 null,前端按「无对比基准」处理 @@ -178,6 +185,23 @@ public class CertAuditServiceImpl implements ICertAuditService { return true; } + @Override + public CertAuditCapabilityVO capability() { + // 与三个审核接口 @PreAuth 同一口径:小程序调度端管理身份(token roleName=admin)查不到菜单码,直接放行; + // 其余账号查菜单。 + CertAuditCapabilityVO capability = new CertAuditCapabilityVO(); + if (mpUserDataScopeSupport.isMpDispatcher()) { + capability.setCanView(true); + capability.setCanAudit(true); + return capability; + } + capability.setCanView(permissionHandler.hasMenu(CertAuditPermissions.DRIVER_VIEW_MENU) + || permissionHandler.hasMenu(CertAuditPermissions.VEHICLE_VIEW_MENU)); + capability.setCanAudit(permissionHandler.hasMenu(CertAuditPermissions.DRIVER_AUDIT_MENU) + || permissionHandler.hasMenu(CertAuditPermissions.VEHICLE_AUDIT_MENU)); + return capability; + } + /** * 更新认证对象的展示缓存,并在通过时把生效快照指针切到这条记录 *

diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertAuditPermissions.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertAuditPermissions.java index 4817baaf..ec31f27d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertAuditPermissions.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertAuditPermissions.java @@ -28,33 +28,66 @@ package org.springblade.transport.support; /** * 认证审核的权限表达式 *

- * 用菜单码而不是角色名。本项目的权限走「角色-菜单」授权,写死角色名有两个坑: - * 角色别名可能对不上(曾写成 {@code admin},而库里只有 {@code administrator}), - * 以及会漏掉实际承担审核的岗位(例如「运管」)。 + * 四个菜单码:司机查看 {@code cert_certification_audit_driver}、车辆查看 {@code cert_certification_audit_vehicle}、 + * 司机审核 {@code cert_certification_audit_driver_audit}、车辆审核 {@code cert_certification_audit_vehicle_audit}。 *

- * 用表达式而不是 {@code @PreAuth(menu = "...")} 的单值形式,是因为同一组接口 - * 由 {@code bizType} 参数区分、同时服务司机与车辆两侧,单值 menu 表达不了「二者之一」。 + * {@code AUDIT_VIEW} 与 {@code AUDIT_ACTION} 由上述常量拼接,结果仍是编译期常量。 + * 三个审核接口的 {@code @PreAuth} 与 {@code CertAuditServiceImpl#capability()} 使用同一组菜单码常量。 *

- * 表达式最终由框架交给 SpEL 求值,root object 是 {@code AuthFun}, - * 故这里用的函数名必须与它的公开方法一致({@code hasMenu})。 - * {@code CertAuditPermissionsTest} 会实际解析并求值这两个表达式, - * 一旦写错函数名或括号,测试即失败——而不是等到线上请求时抛 500。 + * 小程序调度端管理身份不走菜单码:小程序登录不经过 {@code blade_user},token 的 {@code roleId} + * 是占位值 {@code "0"}、{@code roleName} 取 {@code blade_mp_user.identities}(调度端管理员为 {@code admin}), + * 查不到 {@code blade_role_menu},因此两个表达式末尾各放行一个角色名 {@code admin}。 + *

+ * 表达式由框架交给 SpEL 求值,root object 是 {@code AuthFun},函数名须与它的公开方法一致({@code hasMenu})。 + * {@code CertAuditPermissionsTest} 会解析并求值这两个表达式。 * * @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 DRIVER_VIEW_MENU = "cert_certification_audit_driver"; /** - * 审核动作:司机或车辆任一「审核」按钮权限即可 + * 车辆认证的查看菜单码 + */ + public static final String VEHICLE_VIEW_MENU = "cert_certification_audit_vehicle"; + + /** + * 司机认证的「审核」按钮菜单码 + */ + public static final String DRIVER_AUDIT_MENU = "cert_certification_audit_driver_audit"; + + /** + * 车辆认证的「审核」按钮菜单码 + */ + public static final String VEHICLE_AUDIT_MENU = "cert_certification_audit_vehicle_audit"; + + /** + * 小程序调度端管理身份的角色名 + *

+ * 取值来自 {@code blade_mp_user.identities},由登录时拼进 token 的 {@code roleName}; + * 不是 {@code blade_role} 里的角色别名。 + */ + public static final String MP_DISPATCHER_ROLE = "admin"; + + /** + * 待审列表与审核详情:司机或车辆任一查看菜单,或小程序调度端管理身份 + *

+ * 由上面两个查看菜单码拼成,结果仍是编译期常量,{@code @PreAuth} 可以直接引用。 + */ + public static final String AUDIT_VIEW = + "hasMenu('" + DRIVER_VIEW_MENU + "') or hasMenu('" + VEHICLE_VIEW_MENU + "')" + + " or hasRole('" + MP_DISPATCHER_ROLE + "')"; + + /** + * 审核动作:司机或车辆任一「审核」按钮权限,或小程序调度端管理身份 */ public static final String AUDIT_ACTION = - "hasMenu('cert_certification_audit_driver_audit') or hasMenu('cert_certification_audit_vehicle_audit')"; + "hasMenu('" + DRIVER_AUDIT_MENU + "') or hasMenu('" + VEHICLE_AUDIT_MENU + "')" + + " or hasRole('" + MP_DISPATCHER_ROLE + "')"; private CertAuditPermissions() { } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/CertAuditServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/CertAuditServiceImplTest.java index 5de77bd9..fa461766 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/CertAuditServiceImplTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/CertAuditServiceImplTest.java @@ -11,14 +11,17 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.secure.handler.IPermissionHandler; import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.mapper.TransportCertRecordMapper; import org.springblade.transport.pojo.entity.TransportCertRecord; import org.springblade.transport.pojo.entity.TransportVehicle; +import org.springblade.transport.pojo.vo.CertAuditCapabilityVO; import org.springblade.transport.pojo.vo.CertAuditDetailVO; import org.springblade.transport.service.IDriverService; import org.springblade.transport.service.ITransportCertRecordService; import org.springblade.transport.service.ITransportVehicleService; +import org.springblade.transport.support.MpUserDataScopeSupport; import java.util.Collection; @@ -29,10 +32,15 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springblade.transport.support.CertAuditPermissions.DRIVER_AUDIT_MENU; +import static org.springblade.transport.support.CertAuditPermissions.DRIVER_VIEW_MENU; +import static org.springblade.transport.support.CertAuditPermissions.VEHICLE_AUDIT_MENU; +import static org.springblade.transport.support.CertAuditPermissions.VEHICLE_VIEW_MENU; /** * 管理端认证审核测试。 @@ -50,6 +58,8 @@ class CertAuditServiceImplTest { private TransportCertRecordMapper certRecordMapper; private ITransportVehicleService transportVehicleService; private IDriverService driverService; + private IPermissionHandler permissionHandler; + private MpUserDataScopeSupport mpUserDataScopeSupport; private CertAuditServiceImpl service; @BeforeAll @@ -68,7 +78,10 @@ class CertAuditServiceImplTest { certRecordMapper = mock(TransportCertRecordMapper.class); transportVehicleService = mock(ITransportVehicleService.class); driverService = mock(IDriverService.class); - service = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService); + permissionHandler = mock(IPermissionHandler.class); + mpUserDataScopeSupport = mock(MpUserDataScopeSupport.class); + service = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService, + permissionHandler, mpUserDataScopeSupport); // 目标对象的写入默认成功;「写入一行都没命中」由专门的用例覆盖 when(transportVehicleService.updateById(any(TransportVehicle.class))).thenReturn(true); when(driverService.updateById(any(Driver.class))).thenReturn(true); @@ -349,6 +362,104 @@ class CertAuditServiceImplTest { assertNull(detail.getEffectiveRecordId()); } + @Test + @DisplayName("审核详情:驳回原因回填,供详情页直接展示") + void detailReturnsRejectReason() { + TransportCertRecord record = vehicleRecord(TransportCertRecord.STATUS_REJECTED); + record.setSnapshotJson("{\"vin\":\"NEW\"}"); + record.setRejectReason("照片不清晰"); + when(certRecordService.getById(RECORD_ID)).thenReturn(record); + when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null)); + + assertEquals("照片不清晰", service.detail(RECORD_ID).getRejectReason()); + } + + // ------------------------------------------------------------ 审核能力 + + @Test + @DisplayName("能力判定:只有司机查看菜单时能进审核页,但审核按钮必须禁用") + void capabilityViewWithoutAudit() { + when(permissionHandler.hasMenu(DRIVER_VIEW_MENU)).thenReturn(true); + + CertAuditCapabilityVO capability = service.capability(); + + assertTrue(capability.getCanView(), "有查看菜单就应能查看列表与详情"); + assertFalse(capability.getCanAudit(), "只有查看权限时不得允许审核"); + } + + @Test + @DisplayName("能力判定:司机审核按钮菜单即可审核") + void capabilityAuditWithDriverMenu() { + when(permissionHandler.hasMenu(DRIVER_AUDIT_MENU)).thenReturn(true); + + assertTrue(service.capability().getCanAudit(), "司机审核菜单应放行审核动作"); + } + + @Test + @DisplayName("能力判定:车辆审核按钮菜单即可审核") + void capabilityAuditWithVehicleMenu() { + when(permissionHandler.hasMenu(VEHICLE_AUDIT_MENU)).thenReturn(true); + + assertTrue(service.capability().getCanAudit(), "车辆审核菜单应放行审核动作"); + } + + @Test + @DisplayName("能力判定:一个菜单都没有时两项都是 false,且不抛异常") + void capabilityWithoutAnyMenu() { + CertAuditCapabilityVO capability = service.capability(); + + assertFalse(capability.getCanView(), "无查看菜单时前端应显示无权限空态"); + assertFalse(capability.getCanAudit()); + } + + @Test + @DisplayName("能力判定:只有车辆查看菜单时也能查看列表与详情,但审核按钮禁用") + void capabilityVehicleViewWithoutAudit() { + when(permissionHandler.hasMenu(VEHICLE_VIEW_MENU)).thenReturn(true); + + CertAuditCapabilityVO capability = service.capability(); + + assertTrue(capability.getCanView()); + assertFalse(capability.getCanAudit(), "只有查看权限时不得允许审核"); + } + + @Test + @DisplayName("能力判定:查看与审核菜单分属不同对象时两项都放行") + void capabilityMixedMenus() { + when(permissionHandler.hasMenu(DRIVER_VIEW_MENU)).thenReturn(true); + when(permissionHandler.hasMenu(VEHICLE_AUDIT_MENU)).thenReturn(true); + + CertAuditCapabilityVO capability = service.capability(); + + assertTrue(capability.getCanView()); + assertTrue(capability.getCanAudit()); + } + + @Test + @DisplayName("能力判定:小程序调度端管理身份查不到菜单码,查看与审核都放行") + void capabilityForMpDispatcher() { + // 小程序账号的 token roleId 是占位 "0",菜单表里查不到任何一条,只能按身份放行 + when(mpUserDataScopeSupport.isMpDispatcher()).thenReturn(true); + + CertAuditCapabilityVO capability = service.capability(); + + assertTrue(capability.getCanView(), "调度端管理员应能进审核页"); + assertTrue(capability.getCanAudit(), "调度端管理员应能执行审核"); + verify(permissionHandler, never()).hasMenu(anyString()); + } + + @Test + @DisplayName("能力判定:非小程序调度端仍只按菜单码判定") + void capabilityNonDispatcherUsesMenus() { + when(mpUserDataScopeSupport.isMpDispatcher()).thenReturn(false); + when(permissionHandler.hasMenu(DRIVER_VIEW_MENU)).thenReturn(true); + + CertAuditCapabilityVO capability = service.capability(); + + assertTrue(capability.getCanView()); + assertFalse(capability.getCanAudit(), "身份分支不放行时,仍要审核菜单码才能审核"); + } + // -------------------------------------------- 驳回原因的展示缓存 @Test diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java index fa1671b7..0fee6807 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.secure.handler.IPermissionHandler; import org.springblade.transport.pojo.dto.PersonCertSubmitRequest; import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.mapper.TransportCertRecordMapper; @@ -17,6 +18,7 @@ import org.springblade.transport.service.IDriverAppService; import org.springblade.transport.service.IDriverService; import org.springblade.transport.service.ITransportCertRecordService; import org.springblade.transport.service.ITransportVehicleService; +import org.springblade.transport.support.MpUserDataScopeSupport; import java.time.LocalDate; import java.util.HashMap; @@ -97,7 +99,8 @@ class PersonCertRoundTripTest { driverCertService = new DriverCertServiceImpl(driverAppService, driverService, transportVehicleService, certRecordService); - certAuditService = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService); + certAuditService = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService, + mock(IPermissionHandler.class), mock(MpUserDataScopeSupport.class)); } @Test 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 index 4422d9cd..833d0d3d 100644 --- 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 @@ -82,11 +82,47 @@ class CertAuditPermissionsTest { 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 角色,且会漏掉「运管」岗位 + // 角色放行只针对小程序调度端管理身份(blade_mp_user.identities 里的 admin,登录后写进 token 的 roleName), + // 不能改用 hasAnyRole 把 blade_role 的 administrator 也一并放行 + assertTrue(CertAuditPermissions.AUDIT_VIEW.contains("hasRole('" + CertAuditPermissions.MP_DISPATCHER_ROLE + "')")); + assertTrue(CertAuditPermissions.AUDIT_ACTION.contains("hasRole('" + CertAuditPermissions.MP_DISPATCHER_ROLE + "')")); assertFalse(CertAuditPermissions.AUDIT_VIEW.contains("hasAnyRole")); assertFalse(CertAuditPermissions.AUDIT_ACTION.contains("hasAnyRole")); } + @Test + @DisplayName("小程序调度端管理身份(token roleName=admin)无菜单码也可查看与审核") + void mpDispatcherRoleAllowed() { + AuthFun mpDispatcher = mock(AuthFun.class); + when(mpDispatcher.hasRole("admin")).thenReturn(true); + + assertTrue(evaluate(CertAuditPermissions.AUDIT_VIEW, mpDispatcher), + "小程序账号没有 roleId,查不到菜单码"); + assertTrue(evaluate(CertAuditPermissions.AUDIT_ACTION, mpDispatcher), + "小程序调度端管理员需要能执行审核"); + } + + @Test + @DisplayName("小程序司机身份(token roleName=driver)不得进入审核") + void mpDriverRoleDenied() { + AuthFun mpDriver = mock(AuthFun.class); + when(mpDriver.hasRole("driver")).thenReturn(true); + + assertFalse(evaluate(CertAuditPermissions.AUDIT_VIEW, mpDriver)); + assertFalse(evaluate(CertAuditPermissions.AUDIT_ACTION, mpDriver)); + } + + @Test + @DisplayName("超管角色名(administrator)不因角色放行,Web 侧仍须菜单码") + void administratorRoleAloneDoesNotPass() { + AuthFun administrator = mock(AuthFun.class); + when(administrator.hasRole("administrator")).thenReturn(true); + + assertFalse(evaluate(CertAuditPermissions.AUDIT_VIEW, administrator), + "表达式只放行 admin 角色名;超管走 hasMenu 由框架内部放行"); + assertFalse(evaluate(CertAuditPermissions.AUDIT_ACTION, administrator)); + } + /** * 用与框架相同的方式求值:AuthFun 作为 root object */