Merge remote-tracking branch 'websoft/dev'

This commit is contained in:
2026-10-26 13:49:08 +08:00
11 changed files with 361 additions and 19 deletions
@@ -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<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,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<TransportCertRecordVO> 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;
}
/**
* 更新认证对象的展示缓存,并在通过时把生效快照指针切到这条记录
* <p>
@@ -28,33 +28,66 @@ package org.springblade.transport.support;
/**
* 认证审核的权限表达式
* <p>
* 用菜单码而不是角色名。本项目的权限走「角色-菜单」授权,写死角色名有两个坑:
* 角色别名可能对不上(曾写成 {@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}。
* <p>
* 用表达式而不是 {@code @PreAuth(menu = "...")} 的单值形式,是因为同一组接口
* 由 {@code bizType} 参数区分、同时服务司机与车辆两侧,单值 menu 表达不了「二者之一」。
* {@code AUDIT_VIEW} 与 {@code AUDIT_ACTION} 由上述常量拼接,结果仍是编译期常量。
* 三个审核接口的 {@code @PreAuth} 与 {@code CertAuditServiceImpl#capability()} 使用同一组菜单码常量。
* <p>
* 表达式最终由框架交给 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}。
* <p>
* 表达式由框架交给 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";
/**
* 小程序调度端管理身份的角色名
* <p>
* 取值来自 {@code blade_mp_user.identities},由登录时拼进 token 的 {@code roleName};
* 不是 {@code blade_role} 里的角色别名。
*/
public static final String MP_DISPATCHER_ROLE = "admin";
/**
* 待审列表与审核详情:司机或车辆任一查看菜单,或小程序调度端管理身份
* <p>
* 由上面两个查看菜单码拼成,结果仍是编译期常量,{@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() {
}