🔀 合并票 05 认证审核能力判定接口

This commit is contained in:
2026-10-26 03:16:20 +08:00
7 changed files with 166 additions and 4 deletions
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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;
/**
* 认证审核能力
* <p>
* 小程序端进入审核页前先取它决定空态与按钮禁用。没有权限时两个字段为 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;
}
@@ -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<CertAuditCapabilityVO> capability() {
return R.data(certAuditService.capability());
}
}
@@ -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);
/**
* 当前登录用户的审核能力:能否查看、能否审核
* <p>
* 小程序端据此决定无权限空态与审核按钮禁用。两项都按菜单码判断,
* 没有权限时返回 false,**不抛异常**;未登录仍由全局鉴权链拦截,与其它接口一致。
* <p>
* 只读,无副作用;不带租户或对象参数,答案与具体记录无关。
*
* @return 审核能力,两个字段均不为 null
*/
CertAuditCapabilityVO capability();
}
@@ -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<TransportCertRecordVO> 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;
}
/**
* 更新认证对象的展示缓存,并在通过时把生效快照指针切到这条记录
* <p>
@@ -35,6 +35,9 @@ package org.springblade.transport.support;
* 用表达式而不是 {@code @PreAuth(menu = "...")} 的单值形式,是因为同一组接口
* 由 {@code bizType} 参数区分、同时服务司机与车辆两侧,单值 menu 表达不了「二者之一」。
* <p>
* 四个菜单码在本类集中定义,权限表达式与能力判定接口都只引用这些常量,
* 避免同一串菜单码在多处各写一遍、改了一处漏掉另一处。
* <p>
* 表达式最终由框架交给 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";
/**
* 待审列表与审核详情:司机或车辆任一审核菜单即可
* <p>
* 由上面两个查看菜单码拼成,结果仍是编译期常量,故 {@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() {
}
@@ -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
@@ -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