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 有两个容易写坏、写坏了又不会报错的地方: + *

+ * 这里直接编译 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 '驾驶车辆车牌号,多个用逗号分隔';