Merge remote-tracking branch 'websoft/dev'
This commit is contained in:
@@ -144,7 +144,7 @@ public class MpLoginSupport {
|
||||
}
|
||||
try {
|
||||
R<MpUser> result = mpUserClient.userByOpenid(tenantId, openid);
|
||||
return isSuccess(result) ? result.getData() : null;
|
||||
return presentAccount(isSuccess(result) ? result.getData() : null);
|
||||
} catch (Exception exception) {
|
||||
// 查询失败按未命中处理,随后由 ensureUser 触达系统服务并兜住建号失败
|
||||
log.warn("按 openid 查询小程序账号失败:租户={} openid={}", tenantId, openid, exception);
|
||||
@@ -155,13 +155,25 @@ public class MpLoginSupport {
|
||||
private MpUser findByPhone(String tenantId, String phone) {
|
||||
try {
|
||||
R<MpUser> result = mpUserClient.userByPhone(tenantId, phone);
|
||||
return isSuccess(result) ? result.getData() : null;
|
||||
return presentAccount(isSuccess(result) ? result.getData() : null);
|
||||
} catch (Exception exception) {
|
||||
log.warn("按手机号查询小程序账号失败:租户={} 手机号={}", tenantId, phone, exception);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 远端「查无此账号」不等于客户端拿到 null
|
||||
* <p>
|
||||
* 服务端 R.data(null) 落 HTTP 时被 null 空值序列化写成 {}(BladeBeanSerializerModifier 对
|
||||
* Object 属性 null 输出空对象),Feign 侧于是反序列化出一个 id 为空的 MpUser 实例而不是 null。
|
||||
* 调用方只判 null,会把「没查到」当成「查到了空账号」,越过 ensureUser 建号并带着空主键去改库。
|
||||
* 故一律以主键判定是否命中。
|
||||
*/
|
||||
private static MpUser presentAccount(MpUser account) {
|
||||
return account == null || account.getId() == null ? null : account;
|
||||
}
|
||||
|
||||
/**
|
||||
* ② 查找或创建账号:登录路径上唯一允许让登录失败的动作
|
||||
*/
|
||||
@@ -267,6 +279,12 @@ public class MpLoginSupport {
|
||||
if (account == null || StringUtil.isBlank(openid)) {
|
||||
return;
|
||||
}
|
||||
// 空主键/空租户来自「查无账号」被反序列化成空对象,绑定请求会被系统侧判为缺参 400
|
||||
if (account.getId() == null || StringUtil.isBlank(account.getTenantId())) {
|
||||
log.warn("小程序账号缺少主键或租户,跳过绑定 openid:租户={} 账号={} openid={}",
|
||||
account.getTenantId(), account.getId(), openid);
|
||||
return;
|
||||
}
|
||||
// 撞键场景 openid 已属于另一个账号,明确不迁移,也不再尝试绑定
|
||||
if (openidOccupied) {
|
||||
return;
|
||||
|
||||
@@ -229,6 +229,44 @@ class MpLoginSupportTest {
|
||||
verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("远端查无账号返回空对象(id 为空):按未命中建号,并用真实主键补绑 openid")
|
||||
void emptyAccountFromRemoteIsTreatedAsMiss() {
|
||||
// 服务端 R.data(null) 落 HTTP 是 {},Feign 反序列化成 id 为空的 MpUser 而非 null
|
||||
MpUser created = mpUser(61L, PHONE, null, "driver");
|
||||
when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(new MpUser()));
|
||||
when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(new MpUser()));
|
||||
when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
|
||||
when(mpUserClient.bindOpenId(TENANT_ID, 61L, OPENID)).thenReturn(R.data(Boolean.TRUE));
|
||||
when(mpUserClient.userInfoById(61L)).thenReturn(R.data(userInfo(created, "driver")));
|
||||
when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
|
||||
|
||||
OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("61", user.getUserId());
|
||||
assertNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE), "不该出现换号撞键提示");
|
||||
verify(mpUserClient).ensureUser(any(MpUserEnsureDTO.class));
|
||||
verify(mpUserClient, never()).updateUser(any(MpUser.class));
|
||||
verify(mpUserClient).bindOpenId(TENANT_ID, 61L, OPENID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("两个查询都返回空对象时:不落空账号、不发起带空主键的绑定")
|
||||
void emptyAccountDoesNotLeakNullIdentityIntoBind() {
|
||||
MpUser created = mpUser(62L, PHONE, OPENID, "driver");
|
||||
when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(new MpUser()));
|
||||
when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(new MpUser()));
|
||||
when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
|
||||
when(mpUserClient.userInfoById(62L)).thenReturn(R.data(userInfo(created, "driver")));
|
||||
when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
|
||||
|
||||
OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
|
||||
|
||||
assertEquals("62", user.getUserId());
|
||||
verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
private MpUser mpUser(Long id, String phone, String openid, String identities) {
|
||||
MpUser user = new MpUser();
|
||||
user.setId(id);
|
||||
|
||||
+55
@@ -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;本接口不挂权限表达式。
|
||||
* 列表与详情各自的权限表达式不变。
|
||||
*
|
||||
* @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;
|
||||
|
||||
}
|
||||
+2
@@ -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;
|
||||
|
||||
+9
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
+24
@@ -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>
|
||||
|
||||
+47
-14
@@ -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() {
|
||||
}
|
||||
|
||||
+112
-1
@@ -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
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
+37
-1
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user