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/6] =?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
+ * 小程序端据此决定无权限空态与审核按钮禁用。两项都按菜单码判断,
+ * 没有权限时返回 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
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/6] =?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/6] =?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/6] =?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/6] =?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
+ * 小程序调度端管理身份不走菜单码:小程序登录不经过 {@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
*/
From ccf3d7adf7b98908745966a2cb81b8d37f04a100 Mon Sep 17 00:00:00 2001
From: weicw <594098497@qq.com>
Date: Mon, 26 Oct 2026 13:48:30 +0800
Subject: [PATCH 6/6] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=B0=8F?=
=?UTF-8?q?=E7=A8=8B=E5=BA=8F=E9=87=8D=E6=96=B0=E6=8E=88=E6=9D=83=E5=9B=A0?=
=?UTF-8?q?=E7=A9=BA=E8=B4=A6=E5=8F=B7=E5=88=A4=E7=A9=BA=E5=A4=B1=E8=B4=A5?=
=?UTF-8?q?=E6=8A=A5=20500=20=E4=B8=94=20openid=20=E6=97=A0=E6=B3=95?=
=?UTF-8?q?=E7=BB=91=E5=AE=9A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
system 侧 R.data(null) 被空值序列化写成 {},Feign 侧反序列化出 id 为空的
MpUser 而非 null,被当作「已查到」:换号撞键分支误置 openidOccupied,导致
openid 永不绑定并提示「该微信已绑定其他账号」;手机号查不到账号时还会以
空主键、空租户调用 bindOpenId,系统侧判缺参 400,再被 Feign 降级包成
AssertionError(Error 而非 Exception)逃出 catch (Exception),登录返回 500。
- 新增 presentAccount():以主键判定是否命中,findByOpenid/findByPhone 统一走它
- bindOpenid 入口加守卫:缺主键或租户只记 WARN 后跳过,不再发空参数请求
- 补 2 条单测:空对象 data 视为未命中;不落空主键账号、不发空主键绑定
---
.../auth/support/MpLoginSupport.java | 22 ++++++++++-
.../auth/support/MpLoginSupportTest.java | 38 +++++++++++++++++++
2 files changed, 58 insertions(+), 2 deletions(-)
diff --git a/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java
index ab28cd7f..da5442a4 100644
--- a/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java
+++ b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java
@@ -144,7 +144,7 @@ public class MpLoginSupport {
}
try {
R
+ * 服务端 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;
diff --git a/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java b/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java
index 6d09b316..79284906 100644
--- a/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java
+++ b/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java
@@ -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);