diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java
index ad12a17e..c4bf8a40 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java
@@ -78,5 +78,23 @@ public class TransportCertRecordVO extends TransportCertRecord {
@TableField(exist = false)
@Schema(description = "状态中文文案")
private String statusText;
+ /**
+ * 司机姓名(认证对象档案的**当前**姓名,不是提交时的历史值)
+ *
+ * 司机侧取认证对象本身;车辆侧取提交该记录的司机。
+ * 解析不到档案时为空——存量订正记录里没有账号的那些司机补不出人,如实留空。
+ */
+ @TableField(exist = false)
+ @Schema(description = "司机姓名")
+ private String driverName;
+ /**
+ * 司机手机号(认证对象档案的**当前**手机号,明文下发)
+ *
+ * 审核员靠它联系司机核实材料,故不脱敏;认证快照里从来没有手机号,
+ * 它只有司机档案这一个来源。
+ */
+ @TableField(exist = false)
+ @Schema(description = "司机手机号")
+ private String driverMobile;
}
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 5cbd368a..a7b8f544 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
@@ -63,17 +63,21 @@ public class CertificationAuditController extends BladeController {
@GetMapping("/pending-page")
@ApiOperationSupport(order = 1)
- @Operation(summary = "认证记录列表", description = "按认证对象类型与审核状态分页查询,按提交时间倒序;状态不传时查审核中的记录")
+ @Operation(summary = "认证记录列表", description = "按认证对象类型与审核状态分页查询,按提交时间倒序;状态不传时查审核中的记录。列表同时下发该记录司机的姓名与手机号(司机档案当前值)")
@PreAuth(CertAuditPermissions.AUDIT_VIEW)
public R> pendingPage(
@Parameter(description = "认证对象类型:driver-司机,vehicle-车辆", required = true)
@RequestParam String bizType,
- @Parameter(description = "展示键模糊匹配:司机姓名 / 车牌号")
+ @Parameter(description = "展示键模糊匹配:提交时的司机姓名 / 车牌号")
@RequestParam(required = false) String bizKey,
+ @Parameter(description = "司机姓名模糊匹配:当前姓名,同时兼顾提交时的旧名")
+ @RequestParam(required = false) String driverName,
+ @Parameter(description = "司机手机号模糊匹配:支持只输后四位")
+ @RequestParam(required = false) String driverMobile,
@Parameter(description = "审核状态:0审核中 1已通过 2已驳回 3已作废;不传按0(审核中)处理")
@RequestParam(required = false) Integer status,
Query query) {
- return R.data(certAuditService.pendingPage(bizType, bizKey, status, Condition.getPage(query)));
+ return R.data(certAuditService.pendingPage(bizType, bizKey, driverName, driverMobile, status, Condition.getPage(query)));
}
@GetMapping("/detail")
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportCertRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportCertRecordMapper.java
index fc627ea0..1546e132 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportCertRecordMapper.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportCertRecordMapper.java
@@ -26,7 +26,12 @@
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.apache.ibatis.annotations.Param;
import org.springblade.transport.pojo.entity.TransportCertRecord;
+import org.springblade.transport.pojo.vo.TransportCertRecordVO;
+
+import java.util.List;
/**
* 认证记录 Mapper 接口
@@ -35,4 +40,26 @@ import org.springblade.transport.pojo.entity.TransportCertRecord;
*/
public interface TransportCertRecordMapper extends BaseMapper {
+ /**
+ * 审核列表分页:认证记录 + 该记录的司机姓名与手机号
+ *
+ * 必须走自定义 SQL 而不是单表 Wrapper:司机姓名与手机号取的是司机档案的**当前**值,
+ * 且「按司机姓名/手机号筛选」必须发生在**分页之前**——若在 Java 层补字段再过滤,
+ * 就变成「先分页再过滤」,每页条数与总数都不准。
+ *
+ * @param page 分页参数
+ * @param bizType 认证对象类型:driver-司机,vehicle-车辆
+ * @param status 审核状态:0审核中 1已通过 2已驳回 3已作废
+ * @param bizKey 展示键模糊匹配(提交时的司机姓名 / 车牌号),可为空
+ * @param driverName 司机姓名模糊匹配(当前姓名,并兼顾提交时的旧名),可为空
+ * @param driverMobile 司机手机号模糊匹配,可为空
+ * @return 认证记录列表
+ */
+ List selectAuditPage(IPage page,
+ @Param("bizType") String bizType,
+ @Param("status") Integer status,
+ @Param("bizKey") String bizKey,
+ @Param("driverName") String driverName,
+ @Param("driverMobile") String driverMobile);
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportCertRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportCertRecordMapper.xml
new file mode 100644
index 00000000..3aa7ad5a
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportCertRecordMapper.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 69274414..89af4d43 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
@@ -45,15 +45,22 @@ public interface ICertAuditService {
*
* 审核页按「待审核 / 已通过 / 已驳回」分页签展示,故状态可传;
* 不传时按待审核处理,兼容只想要待审队列的调用方。
+ *
+ * 列表同时下发该记录的司机姓名与手机号,取的是司机档案的**当前**值
+ * (认证记录里的姓名是提交时的历史值,手机号则从来不在快照里)。
+ * 解析不到司机档案的记录,两个字段为空,不回落成提交时的值——
+ * 那会让「同一个人的多条记录显示成不同的人」。
*
- * @param bizType 认证对象类型:driver-司机,vehicle-车辆
- * @param bizKey 展示键模糊匹配,可为空
- * @param status 审核状态:0审核中 1已通过 2已驳回 3已作废;为空时取 0(审核中)
- * @param page 分页参数(由 Controller 用 Condition.getPage 构造)
+ * @param bizType 认证对象类型:driver-司机,vehicle-车辆
+ * @param bizKey 展示键模糊匹配(提交时的司机姓名 / 车牌号),可为空
+ * @param driverName 司机姓名模糊匹配(当前姓名,并兼顾提交时的旧名),可为空
+ * @param driverMobile 司机手机号模糊匹配,可为空
+ * @param status 审核状态:0审核中 1已通过 2已驳回 3已作废;为空时取 0(审核中)
+ * @param page 分页参数(由 Controller 用 Condition.getPage 构造)
* @return 认证记录分页
*/
- IPage pendingPage(String bizType, String bizKey, Integer status,
- IPage page);
+ IPage pendingPage(String bizType, String bizKey, String driverName, String driverMobile,
+ Integer status, IPage page);
/**
* 审核详情:同时返回待审快照与当前生效值,供前端做 diff
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 a7a3a526..b274345c 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
@@ -25,14 +25,13 @@
*/
package org.springblade.transport.service.impl;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
+import org.springblade.transport.mapper.TransportCertRecordMapper;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportCertRecord;
import org.springblade.transport.pojo.entity.TransportVehicle;
@@ -48,6 +47,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
+import java.util.List;
/**
* 认证审核服务实现
@@ -66,24 +66,43 @@ import java.time.LocalDateTime;
public class CertAuditServiceImpl implements ICertAuditService {
private final ITransportCertRecordService certRecordService;
+ private final TransportCertRecordMapper certRecordMapper;
private final ITransportVehicleService transportVehicleService;
private final IDriverService driverService;
@Override
- public IPage pendingPage(String bizType, String bizKey, Integer status,
- IPage page) {
+ public IPage pendingPage(String bizType, String bizKey, String driverName, String driverMobile,
+ Integer status, IPage page) {
if (!CertificationRuleSupport.isValidBizType(bizType)) {
throw new ServiceException("认证对象类型不正确");
}
// 未传状态时按「审核中」处理:审核页默认停在待审页签,
// 只关心待审队列的调用方也不必额外传参
Integer queryStatus = status == null ? TransportCertRecord.STATUS_PENDING : status;
- LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery()
- .eq(TransportCertRecord::getBizType, bizType)
- .eq(TransportCertRecord::getStatus, queryStatus)
- .like(Func.isNotEmpty(bizKey), TransportCertRecord::getBizKey, bizKey)
- .orderByDesc(TransportCertRecord::getSubmitTime);
- return TransportCertRecordWrapper.build().pageVO(certRecordService.page(page, queryWrapper));
+
+ // 司机姓名与手机号、以及按它们的筛选,都要在**分页之前**由 SQL 完成:
+ // 在 Java 层补字段再过滤会变成「先分页再过滤」,条数与总数都不准。
+ List records = certRecordMapper.selectAuditPage(page, bizType, queryStatus,
+ blankToNull(bizKey), blankToNull(driverName), blankToNull(driverMobile));
+ TransportCertRecordWrapper wrapper = TransportCertRecordWrapper.build();
+ records.forEach(wrapper::fillDisplayFields);
+ return page.setRecords(records);
+ }
+
+ /**
+ * 搜索项的空串与纯空白一律当作「没填」
+ *
+ * 否则 {@code " "} 会变成 {@code LIKE '% %'},把「没输入」当成一个真实筛选条件。
+ *
+ * @param value 原始搜索值
+ * @return 去空白后的值;为空时返回 null
+ */
+ private String blankToNull(String value) {
+ if (value == null) {
+ return null;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? null : trimmed;
}
@Override
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
index f6b51273..11976336 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
@@ -352,6 +352,9 @@ public class DriverCertServiceImpl implements IDriverCertService {
String drivingVehicle = Func.isEmpty(driver.getDrivingVehicle())
? normalizedPlate
: driver.getDrivingVehicle() + "," + normalizedPlate;
+ // 「最多 5 辆 + 车牌格式」在写入前校验:字段只有 varchar(100),
+ // 超限内容一旦写进去要么报错、要么被静默截断成半个车牌(还会被当成一辆车)
+ VehiclePlateSupport.validateBoundVehicles(drivingVehicle);
Driver patch = new Driver();
patch.setId(driver.getId());
patch.setDrivingVehicle(drivingVehicle);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
index ee887b7c..277e05fb 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
@@ -43,12 +43,12 @@ import org.springblade.system.pojo.enums.UserSex;
import org.springblade.system.pojo.enums.UserType;
import org.springblade.transport.excel.DriverExcel;
import org.springblade.transport.mapper.DriverMapper;
-import org.springblade.transport.mapper.TransportVehicleMapper;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportVehicle;
import org.springblade.transport.pojo.vo.DriverExpiryStatVO;
import org.springblade.transport.pojo.vo.DriverVO;
import org.springblade.transport.service.IDriverService;
+import org.springblade.transport.support.VehiclePlateSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -68,14 +68,13 @@ public class DriverServiceImpl extends BaseServiceImpl imp
private static final int NAME_MAX_LENGTH = 10;
private static final int ID_CARD_MAX_LENGTH = 18;
private static final int MOBILE_MAX_LENGTH = 20;
- private static final int VEHICLE_NO_MAX_LENGTH = 30;
+ private static final int VEHICLE_NO_MAX_LENGTH = 100;
private static final int SHORT_TEXT_MAX_LENGTH = 50;
private static final int ADDRESS_MAX_LENGTH = 200;
private static final int REMARK_MAX_LENGTH = 200;
private static final int DEFAULT_ENABLED_STATUS = 1;
private static final int DEFAULT_FALSE = 0;
- private final TransportVehicleMapper transportVehicleMapper;
private final IUserClient userClient;
private final ISysClient sysClient;
@@ -250,15 +249,11 @@ public class DriverServiceImpl extends BaseServiceImpl imp
validateLength(driver.getIdCardNo(), ID_CARD_MAX_LENGTH, "身份证号不能超过18字");
validateLength(driver.getMobile(), MOBILE_MAX_LENGTH, "手机号不能超过20字");
validateLength(driver.getEmergencyContactMobile(), MOBILE_MAX_LENGTH, "紧急联系人手机号不能超过20字");
- validateLength(driver.getDrivingVehicle(), VEHICLE_NO_MAX_LENGTH, "驾驶车辆不能超过30字");
- if (Func.isNotEmpty(driver.getDrivingVehicle())) {
- Long vehicleCount = transportVehicleMapper.selectCount(Wrappers.lambdaQuery()
- .eq(TransportVehicle::getIsDeleted, 0)
- .eq(TransportVehicle::getPlateNo, driver.getDrivingVehicle()));
- if (vehicleCount == null || vehicleCount <= 0) {
- throw new ServiceException("驾驶车辆不存在,请重新选择");
- }
- }
+ validateLength(driver.getDrivingVehicle(), VEHICLE_NO_MAX_LENGTH, "驾驶车辆不能超过100字");
+ // 「驾驶车辆」是**逗号并列的多辆车**(小程序每绑一辆就追加一个车牌),
+ // 不是一个车牌。此前这里拿整串去车牌表精确查,绑了两辆车的司机
+ // 在司机管理页一保存就会撞上「驾驶车辆不存在」——校验口径必须与写入方一致。
+ VehiclePlateSupport.validateBoundVehicles(driver.getDrivingVehicle());
validateLength(driver.getDrivingLicenseNo(), SHORT_TEXT_MAX_LENGTH, "驾驶证档案编号不能超过50字");
validateLength(driver.getQualificationNo(), SHORT_TEXT_MAX_LENGTH, "资格证号不能超过50字");
validateLength(driver.getOrganizationName(), SHORT_TEXT_MAX_LENGTH, "所属组织不能超过50字");
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java
index 32462dc5..612e9a1e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java
@@ -41,6 +41,7 @@ import org.springblade.transport.pojo.entity.TransportVehicle;
import org.springblade.transport.pojo.vo.TransportVehicleExpiryStatVO;
import org.springblade.transport.pojo.vo.TransportVehicleVO;
import org.springblade.transport.service.ITransportVehicleService;
+import org.springblade.transport.support.VehiclePlateSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -50,7 +51,6 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -62,7 +62,6 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class TransportVehicleServiceImpl extends BaseServiceImpl implements ITransportVehicleService {
- private static final Pattern PLATE_NO_PATTERN = Pattern.compile("^[\\u4e00-\\u9fa5][A-Z][A-Z0-9挂学警港澳]{5,6}$");
private static final String PLATE_COLOR_BLUE = "蓝牌";
private static final String PLATE_COLOR_YELLOW = "黄牌";
private static final String PLATE_COLOR_GREEN = "绿牌";
@@ -262,7 +261,7 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl
+ * 与小程序端「一个司机最多添加 5 辆」的上限一致。后端此前完全没有这个约束,
+ * 而 {@code driving_vehicle} 只有 varchar(30)——5 个车牌连分隔符要 39 字符,
+ * 第 4 辆起就会写不进去或被静默截断(截断后剩下半个车牌,还会被当成一辆车)。
+ */
+ public static final int MAX_BOUND_VEHICLES = 5;
+
+ /**
+ * 车牌格式:中文省份简称 + 字母 + 5~6 位(含挂/学/警/港/澳等特殊字)
+ *
+ * 与车辆建档时的车牌校验共用同一条规则:能绑到司机名下的车牌,
+ * 必须先能作为一辆车建档,否则两处口径分叉,会出现「绑得上但建不了档」。
+ * 传入值必须是 {@link #normalize} 之后的形态(已去排版字符并转大写)。
+ */
+ private static final Pattern PLATE_NO_PATTERN = Pattern.compile("^[\\u4e00-\\u9fa5][A-Z][A-Z0-9挂学警港澳]{5,6}$");
+
+ /**
+ * 车牌格式是否合法
+ *
+ * @param plateNo 车牌(会先做规范化)
+ * @return 是否合法
+ */
+ public static boolean isValidPlateNo(String plateNo) {
+ String normalized = normalize(plateNo);
+ return Func.isNotEmpty(normalized) && PLATE_NO_PATTERN.matcher(normalized).matches();
+ }
+
+ /**
+ * 校验「驾驶车辆」字段:车辆数上限与逐个车牌的格式
+ *
+ * 规则只在这里维护一处——小程序新增车辆、管理端新增/编辑司机都调它。
+ * 此前管理端把这个字段当作**单个**车牌校验(拿整串去车牌表里精确查),
+ * 而小程序写入的是逗号并列的多辆车,两边口径本就不一致:
+ * 一个绑了两辆车的司机,管理员在司机管理页一保存就会撞上「驾驶车辆不存在」。
+ *
+ * @param drivingVehicle 司机档案里的驾驶车辆字段,可并列多辆
+ * @throws ServiceException 车辆数超过上限,或存在格式非法的车牌
+ */
+ public static void validateBoundVehicles(String drivingVehicle) {
+ List plates = splitPlates(drivingVehicle);
+ if (plates.size() > MAX_BOUND_VEHICLES) {
+ throw new ServiceException("一个司机最多绑定" + MAX_BOUND_VEHICLES + "辆车");
+ }
+ for (String plate : plates) {
+ if (!PLATE_NO_PATTERN.matcher(plate).matches()) {
+ throw new ServiceException("车牌号格式不正确:" + plate);
+ }
+ }
+ }
+
/**
* 按车牌匹配车辆的 SQL 条件:精确匹配 **或** 规范化后匹配
*
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java
index 843a6a92..60b9cea4 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java
@@ -48,12 +48,26 @@ public class TransportCertRecordWrapper extends BaseEntityWrapper
+ * 审核列表走的是自定义 SQL,查出来已经是 VO——它多带司机姓名与手机号两个字段,
+ * 这两个字段实体上没有、无法由实体转换得到,所以那条路径不能走 {@link #entityVO}。
+ * 把填充逻辑单独暴露一次,避免 Service 里再复制一份翻译规则,日后两处走偏。
+ *
+ * @param transportCertRecordVO 认证记录视图对象
+ * @return 同一个对象,便于链式调用
+ */
+ public TransportCertRecordVO fillDisplayFields(TransportCertRecordVO transportCertRecordVO) {
+ transportCertRecordVO.setCreateUserName(UserCache.getUserRealName(transportCertRecordVO.getCreateUser()));
+ transportCertRecordVO.setUpdateUserName(UserCache.getUserRealName(transportCertRecordVO.getUpdateUser()));
+ transportCertRecordVO.setSubmitUserName(UserCache.getUserRealName(transportCertRecordVO.getSubmitUser()));
+ transportCertRecordVO.setAuditUserName(UserCache.getUserRealName(transportCertRecordVO.getAuditUser()));
// 状态文案统一在此翻译,避免小程序、Web、接口各处各造一套说法
- transportCertRecordVO.setStatusText(CertificationRuleSupport.statusText(transportCertRecord.getStatus()));
+ transportCertRecordVO.setStatusText(CertificationRuleSupport.statusText(transportCertRecordVO.getStatus()));
return transportCertRecordVO;
}
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 49776737..e03f9d9d 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
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springblade.core.log.exception.ServiceException;
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.CertAuditDetailVO;
@@ -36,6 +37,7 @@ class CertAuditServiceImplTest {
private static final Long DRIVER_ID = 9001L;
private ITransportCertRecordService certRecordService;
+ private TransportCertRecordMapper certRecordMapper;
private ITransportVehicleService transportVehicleService;
private IDriverService driverService;
private CertAuditServiceImpl service;
@@ -43,9 +45,10 @@ class CertAuditServiceImplTest {
@BeforeEach
void setUp() {
certRecordService = mock(ITransportCertRecordService.class);
+ certRecordMapper = mock(TransportCertRecordMapper.class);
transportVehicleService = mock(ITransportVehicleService.class);
driverService = mock(IDriverService.class);
- service = new CertAuditServiceImpl(certRecordService, transportVehicleService, driverService);
+ service = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService);
}
// ------------------------------------------------------------ 通过
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 2015fc67..deb2de2b 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
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springblade.transport.pojo.dto.PersonCertSubmitRequest;
import org.springblade.transport.pojo.entity.Driver;
+import org.springblade.transport.mapper.TransportCertRecordMapper;
import org.springblade.transport.pojo.entity.TransportCertRecord;
import org.springblade.transport.service.IDriverAppService;
import org.springblade.transport.service.IDriverService;
@@ -38,6 +39,7 @@ class PersonCertRoundTripTest {
private IDriverAppService driverAppService;
private IDriverService driverService;
private ITransportCertRecordService certRecordService;
+ private TransportCertRecordMapper certRecordMapper;
private DriverCertServiceImpl driverCertService;
private CertAuditServiceImpl certAuditService;
@@ -56,6 +58,7 @@ class PersonCertRoundTripTest {
driverAppService = mock(IDriverAppService.class);
driverService = mock(IDriverService.class);
certRecordService = mock(ITransportCertRecordService.class);
+ certRecordMapper = mock(TransportCertRecordMapper.class);
ITransportVehicleService transportVehicleService = mock(ITransportVehicleService.class);
driver = new Driver();
@@ -75,7 +78,7 @@ class PersonCertRoundTripTest {
driverCertService = new DriverCertServiceImpl(driverAppService, driverService,
transportVehicleService, certRecordService);
- certAuditService = new CertAuditServiceImpl(certRecordService, transportVehicleService, driverService);
+ certAuditService = new CertAuditServiceImpl(certRecordService, certRecordMapper, transportVehicleService, driverService);
}
@Test
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertAuditPageSqlTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertAuditPageSqlTest.java
new file mode 100644
index 00000000..f2f7e9f9
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertAuditPageSqlTest.java
@@ -0,0 +1,114 @@
+package org.springblade.transport.support;
+
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.session.Configuration;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 审核列表自定义 SQL 的语句级验证。
+ *
+ * 这条 SQL 有两个容易写坏、写坏了又不会报错的地方:
+ *
+ * - 车型侧要按提交司机定位司机档案。车辆表没有司机列,
+ * 定位靠 {@code dr.user_id = r.submit_user}。少了这个条件,
+ * 车辆记录的司机列会全部取不到值,而页面只是「空着」——不像故障。
+ * - 司机侧不能跟着走 submit_user。存量订正记录没有提交人,
+ * 若司机侧也用 submit_user 定位,23 条有司机的记录反而会全部变空。
+ *
+ * 这里直接编译 Mapper XML 并检查生成出来的语句,把「靠约定」变成「靠语句」。
+ */
+class CertAuditPageSqlTest {
+
+ private static Configuration configuration;
+
+ @BeforeAll
+ static void loadMapperXml() {
+ configuration = new Configuration();
+ String resource = "org/springblade/transport/mapper/TransportCertRecordMapper.xml";
+ try (InputStream input = CertAuditPageSqlTest.class.getClassLoader().getResourceAsStream(resource)) {
+ assertNotNull(input, "找不到 Mapper XML:" + resource + "(它必须与 Mapper 接口同包放在 src/main/java 下)");
+ new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
+ } catch (Exception exception) {
+ throw new IllegalStateException("Mapper XML 解析失败:" + exception.getMessage(), exception);
+ }
+ }
+
+ @Test
+ @DisplayName("无搜索条件:仍按状态与对象类型过滤,并按提交时间倒序")
+ void baseConditions() {
+ String sql = sql(Map.of());
+
+ assertTrue(sql.contains("r.status = ?"), "必须按审核状态过滤");
+ assertTrue(sql.contains("r.biz_type = ?"), "必须按认证对象类型过滤");
+ assertTrue(sql.contains("r.is_deleted = 0"), "逻辑删除的记录不得出现在审核列表");
+ assertTrue(sql.contains("ORDER BY") && sql.contains("submit_time DESC"), "必须按提交时间倒序");
+ }
+
+ @Test
+ @DisplayName("司机档案定位:司机侧按对象主键,车辆侧按提交司机(user_id)")
+ void driverResolution() {
+ String sql = sql(Map.of());
+
+ assertTrue(sql.contains("WHEN r.biz_type = 'driver' THEN r.biz_id"),
+ "司机认证记录的司机就是认证对象本身");
+ assertTrue(sql.contains("dr.user_id = r.submit_user"),
+ "车辆认证记录没有司机列,只能靠提交司机定位——少了它车辆页的司机列会整列取不到值");
+ }
+
+ @Test
+ @DisplayName("司机档案收敛为一条:子查询必须带 ORDER BY ... LIMIT 1")
+ void driverResolutionCollapsesToOneRow() {
+ String sql = sql(Map.of());
+
+ assertTrue(sql.contains("ORDER BY dr.update_time DESC") && sql.contains("LIMIT 1"),
+ "user_id 上没有唯一索引,两条司机档案指向同一用户时会让一条记录在列表里出现多行、总数虚高");
+ }
+
+ @Test
+ @DisplayName("空搜索条件不产生任何 LIKE(不能把「没填」当成筛选条件)")
+ void blankConditionsAddNoFilter() {
+ String sql = sql(Map.of());
+
+ assertFalse(sql.contains("LIKE"), "没传搜索项时不应出现 LIKE:空串会被拼成 LIKE '%%'");
+ }
+
+ @Test
+ @DisplayName("姓名搜索:同时匹配档案当前姓名与提交时的姓名")
+ void nameSearchCoversBothCurrentAndHistorical() {
+ String sql = sql(Map.of("driverName", "黄"));
+
+ assertTrue(sql.contains("d.driver_name LIKE"), "必须能按档案当前姓名搜到");
+ assertTrue(sql.contains("r.biz_key LIKE"), "改名后按旧名也要能搜到,否则审核员得两头试");
+ }
+
+ @Test
+ @DisplayName("手机号搜索:只按司机档案的手机号匹配(快照里从来没有手机号)")
+ void mobileSearchUsesProfileOnly() {
+ String sql = sql(Map.of("driverMobile", "1208"));
+
+ assertTrue(sql.contains("d.mobile LIKE"), "手机号只有司机档案一个来源");
+ }
+
+ private String sql(Map params) {
+ MappedStatement statement = configuration.getMappedStatement(
+ "org.springblade.transport.mapper.TransportCertRecordMapper.selectAuditPage");
+ Map parameters = new HashMap<>(params);
+ parameters.put("bizType", "driver");
+ parameters.put("status", 0);
+ BoundSql boundSql = statement.getBoundSql(parameters);
+ return boundSql.getSql();
+ }
+
+}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/VehiclePlateSupportTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/VehiclePlateSupportTest.java
index 388d848f..445e0419 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/VehiclePlateSupportTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/VehiclePlateSupportTest.java
@@ -1,6 +1,7 @@
package org.springblade.transport.support;
import org.junit.jupiter.api.BeforeAll;
+import org.springblade.core.log.exception.ServiceException;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
@@ -14,6 +15,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -129,6 +131,58 @@ class VehiclePlateSupportTest {
assertTrue(sql.contains("OR"), "两者应是 OR 关系");
}
+ @Test
+ @DisplayName("绑定数上限:5 辆放行,第 6 辆拒绝")
+ void boundVehicleCountLimit() {
+ String five = "桂A32523,桂B11111,桂C22222,桂D33333,桂E44444";
+ VehiclePlateSupport.validateBoundVehicles(five);
+ assertEquals(5, VehiclePlateSupport.splitPlates(five).size());
+
+ // 不抛异常就是通过;第 6 辆必须拦下,否则字段放不下会被静默截断
+ ServiceException exception = assertThrows(ServiceException.class,
+ () -> VehiclePlateSupport.validateBoundVehicles(five + ",桂F55555"));
+ assertTrue(exception.getMessage().contains("最多绑定5辆车"), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("绑定校验:空值与单辆车都放行")
+ void boundVehicleEmptyAndSingle() {
+ VehiclePlateSupport.validateBoundVehicles(null);
+ VehiclePlateSupport.validateBoundVehicles("");
+ VehiclePlateSupport.validateBoundVehicles("桂A32523");
+ }
+
+ @Test
+ @DisplayName("绑定校验:车牌格式不合法时拒绝,且报出是哪个车牌")
+ void boundVehicleRejectsBadPlate() {
+ ServiceException exception = assertThrows(ServiceException.class,
+ () -> VehiclePlateSupport.validateBoundVehicles("桂A32523,乱写的车牌"));
+ assertTrue(exception.getMessage().contains("车牌号格式不正确"), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("车牌格式:普通、新能源(8位)、挂车/学车/警车都合法")
+ void validPlateFormats() {
+ assertTrue(VehiclePlateSupport.isValidPlateNo("桂A32523"));
+ assertTrue(VehiclePlateSupport.isValidPlateNo("沪AE31312"));
+ assertTrue(VehiclePlateSupport.isValidPlateNo("桂A1234挂"));
+ assertTrue(VehiclePlateSupport.isValidPlateNo("京A1234学"));
+ assertTrue(VehiclePlateSupport.isValidPlateNo("桂A1234警"));
+ // 写法差异不影响判定:小写与带间隔符的车牌先规范化再比对
+ assertTrue(VehiclePlateSupport.isValidPlateNo("桂a32523"));
+ assertTrue(VehiclePlateSupport.isValidPlateNo("桂A·32523"));
+ }
+
+ @Test
+ @DisplayName("车牌格式:空值、乱码、位数不对一律不合法")
+ void invalidPlateFormats() {
+ assertFalse(VehiclePlateSupport.isValidPlateNo(null));
+ assertFalse(VehiclePlateSupport.isValidPlateNo(""));
+ assertFalse(VehiclePlateSupport.isValidPlateNo("乱写的车牌"));
+ assertFalse(VehiclePlateSupport.isValidPlateNo("A32523"));
+ assertFalse(VehiclePlateSupport.isValidPlateNo("桂A325"));
+ }
+
@Test
@DisplayName("空车牌列表不产生任何条件(避免 IN () 语法错误)")
void emptyPlateListAddsNoCondition() {
diff --git a/doc/sql/transport/blade_transport_driver.sql b/doc/sql/transport/blade_transport_driver.sql
index d21eb393..19809bf5 100644
--- a/doc/sql/transport/blade_transport_driver.sql
+++ b/doc/sql/transport/blade_transport_driver.sql
@@ -13,7 +13,7 @@ CREATE TABLE `blade_transport_driver` (
`education` varchar(20) DEFAULT NULL COMMENT '学历',
`address_region` varchar(100) DEFAULT NULL COMMENT '地址区划',
`address` varchar(200) DEFAULT NULL COMMENT '详细地址',
- `driving_vehicle` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '驾驶车辆车牌号',
+ `driving_vehicle` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '驾驶车辆车牌号,多个用逗号分隔',
`posts` varchar(100) DEFAULT NULL COMMENT '岗位,多个使用逗号分隔',
`id_card_front` varchar(1000) DEFAULT NULL COMMENT '身份证正面照',
`id_card_back` varchar(1000) DEFAULT NULL COMMENT '身份证反面照',
diff --git a/doc/sql/transport/blade_transport_driver_driving_vehicle_widen_20261003.sql b/doc/sql/transport/blade_transport_driver_driving_vehicle_widen_20261003.sql
new file mode 100644
index 00000000..34194030
--- /dev/null
+++ b/doc/sql/transport/blade_transport_driver_driving_vehicle_widen_20261003.sql
@@ -0,0 +1,13 @@
+-- ----------------------------
+-- 司机「驾驶车辆」字段扩长:varchar(30) -> varchar(100)
+-- ----------------------------
+-- 一个司机最多绑定 5 辆车(与小程序端「车辆认证一个司机最多添加 5 辆」一致),
+-- 该字段是**逗号并列的车牌列表**,5 个普通车牌连分隔符要 39 字符,
+-- 新能源号牌(8 位)要 44 字符 —— 30 字符连 4 辆车都放不下。
+-- 超长写入的后果不是报错而是**静默截断**:字段里会留下半个车牌,
+-- 而拆车牌的规则会把这段残渣当成一辆车,司机名下于是多出一辆不存在的车。
+--
+-- 排序规则保持不变(utf8mb4_general_ci):车辆表与认证记录表都用这个规则,
+-- 三张表之间已有按车牌的关联查询,改规则会引发 Illegal mix of collations。
+ALTER TABLE `blade_transport_driver`
+ MODIFY COLUMN `driving_vehicle` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '驾驶车辆车牌号,多个用逗号分隔';