From ec926afa467aaf8320d160855cea9c7e3710a82d Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Mon, 26 Oct 2026 03:14:05 +0800 Subject: [PATCH 1/5] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9E=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E5=AE=A1=E6=A0=B8=E8=83=BD=E5=8A=9B=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/vo/CertAuditCapabilityVO.java | 55 +++++++++++++++++++ .../CertificationAuditController.java | 9 +++ .../transport/service/ICertAuditService.java | 13 +++++ .../service/impl/CertAuditServiceImpl.java | 15 +++++ .../support/CertAuditPermissions.java | 29 +++++++++- .../impl/CertAuditServiceImplTest.java | 45 ++++++++++++++- .../service/impl/PersonCertRoundTripTest.java | 4 +- 7 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditCapabilityVO.java 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..67ba55fb --- /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, + * 而不是让接口返回 403——列表与详情各自仍挂权限表达式,这里只回答「能不能」。 + * + * @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/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..86dcf0da 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,而不是被拦成 403 + @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..ccd59c44 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,12 +41,14 @@ 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.wrapper.TransportCertRecordWrapper; import org.springframework.stereotype.Service; @@ -75,6 +78,7 @@ public class CertAuditServiceImpl implements ICertAuditService { private final TransportCertRecordMapper certRecordMapper; private final ITransportVehicleService transportVehicleService; private final IDriverService driverService; + private final IPermissionHandler permissionHandler; @Override public IPage pendingPage(String bizType, String bizKey, String driverName, String driverMobile, @@ -178,6 +182,17 @@ public class CertAuditServiceImpl implements ICertAuditService { return true; } + @Override + public CertAuditCapabilityVO capability() { + // 菜单码与三个审核接口的 @PreAuth 表达式同源,不另写字面量,也不在这里求值表达式 + CertAuditCapabilityVO capability = new CertAuditCapabilityVO(); + 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..8a09c5b7 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 @@ -35,6 +35,9 @@ package org.springblade.transport.support; * 用表达式而不是 {@code @PreAuth(menu = "...")} 的单值形式,是因为同一组接口 * 由 {@code bizType} 参数区分、同时服务司机与车辆两侧,单值 menu 表达不了「二者之一」。 *

+ * 四个菜单码在本类集中定义,权限表达式与能力判定接口都只引用这些常量, + * 避免同一串菜单码在多处各写一遍、改了一处漏掉另一处。 + *

* 表达式最终由框架交给 SpEL 求值,root object 是 {@code AuthFun}, * 故这里用的函数名必须与它的公开方法一致({@code hasMenu})。 * {@code CertAuditPermissionsTest} 会实际解析并求值这两个表达式, @@ -44,17 +47,39 @@ package org.springblade.transport.support; */ public final class CertAuditPermissions { + /** + * 司机认证的查看菜单码 + */ + 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 @PreAuth} 可以直接引用。 */ public static final String AUDIT_VIEW = - "hasMenu('cert_certification_audit_driver') or hasMenu('cert_certification_audit_vehicle')"; + "hasMenu('" + DRIVER_VIEW_MENU + "') or hasMenu('" + VEHICLE_VIEW_MENU + "')"; /** * 审核动作:司机或车辆任一「审核」按钮权限即可 */ 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 + "')"; 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..56de6146 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,10 +11,12 @@ 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; @@ -50,6 +52,7 @@ class CertAuditServiceImplTest { private TransportCertRecordMapper certRecordMapper; private ITransportVehicleService transportVehicleService; private IDriverService driverService; + private IPermissionHandler permissionHandler; private CertAuditServiceImpl service; @BeforeAll @@ -68,7 +71,9 @@ 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); + service = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService, + permissionHandler); // 目标对象的写入默认成功;「写入一行都没命中」由专门的用例覆盖 when(transportVehicleService.updateById(any(TransportVehicle.class))).thenReturn(true); when(driverService.updateById(any(Driver.class))).thenReturn(true); @@ -349,6 +354,44 @@ class CertAuditServiceImplTest { assertNull(detail.getEffectiveRecordId()); } + // ------------------------------------------------------------ 审核能力 + + @Test + @DisplayName("能力判定:只有司机查看菜单时能进审核页,但审核按钮必须禁用") + void capabilityViewWithoutAudit() { + when(permissionHandler.hasMenu("cert_certification_audit_driver")).thenReturn(true); + + CertAuditCapabilityVO capability = service.capability(); + + assertTrue(capability.getCanView(), "有查看菜单就应能查看列表与详情"); + assertFalse(capability.getCanAudit(), "只有查看权限时不得允许审核"); + } + + @Test + @DisplayName("能力判定:司机审核按钮菜单即可审核") + void capabilityAuditWithDriverMenu() { + when(permissionHandler.hasMenu("cert_certification_audit_driver_audit")).thenReturn(true); + + assertTrue(service.capability().getCanAudit(), "司机审核菜单应放行审核动作"); + } + + @Test + @DisplayName("能力判定:车辆审核按钮菜单即可审核") + void capabilityAuditWithVehicleMenu() { + when(permissionHandler.hasMenu("cert_certification_audit_vehicle_audit")).thenReturn(true); + + assertTrue(service.capability().getCanAudit(), "车辆审核菜单应放行审核动作"); + } + + @Test + @DisplayName("能力判定:一个菜单都没有时两项都是 false,且不抛异常") + void capabilityWithoutAnyMenu() { + CertAuditCapabilityVO capability = service.capability(); + + assertFalse(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..b5b14c16 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; @@ -97,7 +98,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)); } @Test From e0ff423de0aa5f8566204fcdc27e3faf4b07517e Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Mon, 26 Oct 2026 03:22:35 +0800 Subject: [PATCH 2/5] =?UTF-8?q?:bug:=20=E5=AE=A1=E6=A0=B8=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E8=BF=94=E5=9B=9E=E9=A9=B3=E5=9B=9E=E5=8E=9F=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/springblade/transport/pojo/vo/CertAuditDetailVO.java | 2 ++ .../transport/service/impl/CertAuditServiceImpl.java | 1 + 2 files changed, 3 insertions(+) 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/service/impl/CertAuditServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java index ccd59c44..65310611 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 @@ -134,6 +134,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,前端按「无对比基准」处理 From aac08bcbaaaad911e7b5ee114830b0fb657ddd4d Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Mon, 26 Oct 2026 03:39:55 +0800 Subject: [PATCH 3/5] =?UTF-8?q?:memo:=20=E8=AE=A4=E8=AF=81=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E6=B3=A8=E9=87=8A=E6=94=B9=E4=B8=BA=E4=BA=8B=E5=AE=9E?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E5=B9=B6=E7=BB=9F=E4=B8=80=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E7=A0=81=E5=B8=B8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/vo/CertAuditCapabilityVO.java | 4 ++-- .../CertificationAuditController.java | 2 +- .../service/impl/CertAuditServiceImpl.java | 2 +- .../support/CertAuditPermissions.java | 22 +++++++------------ .../impl/CertAuditServiceImplTest.java | 9 +++++--- 5 files changed, 18 insertions(+), 21 deletions(-) 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 index 67ba55fb..7b71e537 100644 --- 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 @@ -34,8 +34,8 @@ import java.io.Serializable; /** * 认证审核能力 *

- * 小程序端进入审核页前先取它决定空态与按钮禁用。没有权限时两个字段为 false, - * 而不是让接口返回 403——列表与详情各自仍挂权限表达式,这里只回答「能不能」。 + * 无权限时两个字段为 false;本接口不挂权限表达式。 + * 列表与详情各自的权限表达式不变。 * * @author Chill */ 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 86dcf0da..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 @@ -101,7 +101,7 @@ public class CertificationAuditController extends BladeController { return R.status(certAuditService.audit(id, Boolean.TRUE.equals(pass), rejectReason)); } - // 不挂 @PreAuth:没有审核权限时也要能拿到 false,而不是被拦成 403 + // 本接口不挂 @PreAuth;无权限时返回 false。 @GetMapping("/capability") @ApiOperationSupport(order = 4) @Operation(summary = "审核能力判定", description = "无权限返回 false,不抛 403,供小程序决定空态与按钮禁用") 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 65310611..112231de 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 @@ -185,7 +185,7 @@ public class CertAuditServiceImpl implements ICertAuditService { @Override public CertAuditCapabilityVO capability() { - // 菜单码与三个审核接口的 @PreAuth 表达式同源,不另写字面量,也不在这里求值表达式 + // 使用与三个审核接口 @PreAuth 相同的菜单码常量;表达式由框架求值,此处直接查菜单。 CertAuditCapabilityVO capability = new CertAuditCapabilityVO(); capability.setCanView(permissionHandler.hasMenu(CertAuditPermissions.DRIVER_VIEW_MENU) || permissionHandler.hasMenu(CertAuditPermissions.VEHICLE_VIEW_MENU)); 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 8a09c5b7..aa1d17a0 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,20 +28,14 @@ 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。 + * 表达式由框架交给 SpEL 求值,root object 是 {@code AuthFun},函数名须与它的公开方法一致({@code hasMenu})。 + * {@code CertAuditPermissionsTest} 会解析并求值这两个表达式。 * * @author Chill */ @@ -68,9 +62,9 @@ public final class CertAuditPermissions { public static final String VEHICLE_AUDIT_MENU = "cert_certification_audit_vehicle_audit"; /** - * 待审列表与审核详情:司机或车辆任一审核菜单即可 + * 待审列表与审核详情:司机或车辆任一查看菜单即可 *

- * 由上面两个查看菜单码拼成,结果仍是编译期常量,故 {@code @PreAuth} 可以直接引用。 + * 由上面两个查看菜单码拼成,结果仍是编译期常量,{@code @PreAuth} 可以直接引用。 */ public static final String AUDIT_VIEW = "hasMenu('" + DRIVER_VIEW_MENU + "') or hasMenu('" + VEHICLE_VIEW_MENU + "')"; 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 56de6146..b780f977 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 @@ -35,6 +35,9 @@ 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; /** * 管理端认证审核测试。 @@ -359,7 +362,7 @@ class CertAuditServiceImplTest { @Test @DisplayName("能力判定:只有司机查看菜单时能进审核页,但审核按钮必须禁用") void capabilityViewWithoutAudit() { - when(permissionHandler.hasMenu("cert_certification_audit_driver")).thenReturn(true); + when(permissionHandler.hasMenu(DRIVER_VIEW_MENU)).thenReturn(true); CertAuditCapabilityVO capability = service.capability(); @@ -370,7 +373,7 @@ class CertAuditServiceImplTest { @Test @DisplayName("能力判定:司机审核按钮菜单即可审核") void capabilityAuditWithDriverMenu() { - when(permissionHandler.hasMenu("cert_certification_audit_driver_audit")).thenReturn(true); + when(permissionHandler.hasMenu(DRIVER_AUDIT_MENU)).thenReturn(true); assertTrue(service.capability().getCanAudit(), "司机审核菜单应放行审核动作"); } @@ -378,7 +381,7 @@ class CertAuditServiceImplTest { @Test @DisplayName("能力判定:车辆审核按钮菜单即可审核") void capabilityAuditWithVehicleMenu() { - when(permissionHandler.hasMenu("cert_certification_audit_vehicle_audit")).thenReturn(true); + when(permissionHandler.hasMenu(VEHICLE_AUDIT_MENU)).thenReturn(true); assertTrue(service.capability().getCanAudit(), "车辆审核菜单应放行审核动作"); } From 0ebba9d6b7963c426f53816b0962fbae777be4b4 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Mon, 26 Oct 2026 10:49:09 +0800 Subject: [PATCH 4/5] =?UTF-8?q?:white=5Fcheck=5Fmark:=20=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E8=AF=A6=E6=83=85=E9=A9=B3=E5=9B=9E=E5=8E=9F?= =?UTF-8?q?=E5=9B=A0=E4=B8=8E=E8=BD=A6=E8=BE=86=E6=9F=A5=E7=9C=8B=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E8=83=BD=E5=8A=9B=E5=88=A4=E5=AE=9A=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/CertAuditServiceImplTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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 b780f977..df9bbd10 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 @@ -38,6 +38,7 @@ 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; /** * 管理端认证审核测试。 @@ -357,6 +358,18 @@ 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 @@ -395,6 +408,29 @@ class CertAuditServiceImplTest { 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 From b7d922077de44cbe3248645842fee519c8168594 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Mon, 26 Oct 2026 11:38:50 +0800 Subject: [PATCH 5/5] =?UTF-8?q?:bug:=20=E8=AE=A4=E8=AF=81=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E6=94=BE=E8=A1=8C=E5=B0=8F=E7=A8=8B=E5=BA=8F=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E7=AB=AF=E7=AE=A1=E7=90=86=E8=BA=AB=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/CertAuditServiceImpl.java | 10 ++++- .../support/CertAuditPermissions.java | 22 +++++++++-- .../impl/CertAuditServiceImplTest.java | 31 ++++++++++++++- .../service/impl/PersonCertRoundTripTest.java | 3 +- .../support/CertAuditPermissionsTest.java | 38 ++++++++++++++++++- 5 files changed, 96 insertions(+), 8 deletions(-) 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 112231de..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 @@ -50,6 +50,7 @@ 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; @@ -79,6 +80,7 @@ public class CertAuditServiceImpl implements ICertAuditService { 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, @@ -185,8 +187,14 @@ public class CertAuditServiceImpl implements ICertAuditService { @Override public CertAuditCapabilityVO capability() { - // 使用与三个审核接口 @PreAuth 相同的菜单码常量;表达式由框架求值,此处直接查菜单。 + // 与三个审核接口 @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) 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 aa1d17a0..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 @@ -34,6 +34,10 @@ package org.springblade.transport.support; * {@code AUDIT_VIEW} 与 {@code AUDIT_ACTION} 由上述常量拼接,结果仍是编译期常量。 * 三个审核接口的 {@code @PreAuth} 与 {@code CertAuditServiceImpl#capability()} 使用同一组菜单码常量。 *

+ * 小程序调度端管理身份不走菜单码:小程序登录不经过 {@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} 会解析并求值这两个表达式。 * @@ -62,18 +66,28 @@ public final class CertAuditPermissions { 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 + "')"; + "hasMenu('" + DRIVER_VIEW_MENU + "') or hasMenu('" + VEHICLE_VIEW_MENU + "')" + + " or hasRole('" + MP_DISPATCHER_ROLE + "')"; /** - * 审核动作:司机或车辆任一「审核」按钮权限即可 + * 审核动作:司机或车辆任一「审核」按钮权限,或小程序调度端管理身份 */ public static final String AUDIT_ACTION = - "hasMenu('" + DRIVER_AUDIT_MENU + "') or hasMenu('" + VEHICLE_AUDIT_MENU + "')"; + "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 df9bbd10..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 @@ -21,6 +21,7 @@ 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; @@ -31,6 +32,7 @@ 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; @@ -57,6 +59,7 @@ class CertAuditServiceImplTest { private ITransportVehicleService transportVehicleService; private IDriverService driverService; private IPermissionHandler permissionHandler; + private MpUserDataScopeSupport mpUserDataScopeSupport; private CertAuditServiceImpl service; @BeforeAll @@ -76,8 +79,9 @@ class CertAuditServiceImplTest { transportVehicleService = mock(ITransportVehicleService.class); driverService = mock(IDriverService.class); permissionHandler = mock(IPermissionHandler.class); + mpUserDataScopeSupport = mock(MpUserDataScopeSupport.class); service = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService, - permissionHandler); + permissionHandler, mpUserDataScopeSupport); // 目标对象的写入默认成功;「写入一行都没命中」由专门的用例覆盖 when(transportVehicleService.updateById(any(TransportVehicle.class))).thenReturn(true); when(driverService.updateById(any(Driver.class))).thenReturn(true); @@ -431,6 +435,31 @@ class CertAuditServiceImplTest { 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 b5b14c16..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 @@ -18,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; @@ -99,7 +100,7 @@ class PersonCertRoundTripTest { driverCertService = new DriverCertServiceImpl(driverAppService, driverService, transportVehicleService, certRecordService); certAuditService = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService, - mock(IPermissionHandler.class)); + 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 */