🐛 修 code-review 指出的三处问题:车牌匹配、提交人姓名为空、分页写法
1) **车牌匹配漏了规范化兜底(最严重)** DriverWaybillServiceImpl.assertCertified 把 splitPlates 返回的**规范化**车牌 直接与 plate_no 原始列做精确 IN。库中存在带间隔符/横线/大小写差异的车牌时 会命中 0 行,导致「名下无可用车辆」→ **已认证的司机被误判为未认证而拒绝接单**, 与本次改造的核心验收「已认证的车重新提交后仍可正常接单」相悖。 DriverCertServiceImpl.findByPlateWithinBound 同样问题:先精确 IN 取候选再规范化 filter,而 filter 只能收窄不能放宽,漏掉的候选永远找不回来(等于没做规范化匹配)。 改法:把「精确 IN 或 规范化后相等」抽成 VehiclePlateSupport.applyPlateMatch, 三处调用点(含原先自己实现的 DriverAppServiceImpl.myVehicles)统一使用。 并加测试断言 SQL 表达式覆盖 Java 侧剔除的全部字符、且必须带 OR 兜底, 防止两边日后悄悄分叉。 2) **审核详情的提交人姓名恒为空**:CertAuditDetailVO.submitUserName 声明了却从未赋值, 违反「VO 展示字段须由服务端用 UserCache 填好」的规范。已补。 3) **分页未走项目统一写法**:原用 Page.of 手搓,全模块无先例。改为 Controller 收 Query + Condition.getPage(query),与 AccidentRecordController 等一致。 Query 绑定的正是 current/size,故三端契约不受影响。 顺带删除无任何调用者的 ICertAuditService.getRecordOrNull(推测性抽象 + 纯转发)。 82 个测试通过。
This commit is contained in:
+4
-3
@@ -32,6 +32,8 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
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.CertAuditDetailVO;
|
||||
@@ -68,9 +70,8 @@ public class CertificationAuditController extends BladeController {
|
||||
@RequestParam String bizType,
|
||||
@Parameter(description = "展示键模糊匹配:司机姓名 / 车牌号")
|
||||
@RequestParam(required = false) String bizKey,
|
||||
@Parameter(description = "页码") @RequestParam(required = false) Integer current,
|
||||
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size) {
|
||||
return R.data(certAuditService.pendingPage(bizType, bizKey, current, size));
|
||||
Query query) {
|
||||
return R.data(certAuditService.pendingPage(bizType, bizKey, Condition.getPage(query)));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
|
||||
+4
-12
@@ -44,12 +44,12 @@ public interface ICertAuditService {
|
||||
* 待审记录分页(按认证对象类型)
|
||||
*
|
||||
* @param bizType 认证对象类型:driver-司机,vehicle-车辆
|
||||
* @param bizKey 展示键模糊匹配,可为空
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 每页条数
|
||||
* @param bizKey 展示键模糊匹配,可为空
|
||||
* @param page 分页参数(由 Controller 用 Condition.getPage 构造)
|
||||
* @return 待审记录分页
|
||||
*/
|
||||
IPage<TransportCertRecordVO> pendingPage(String bizType, String bizKey, Integer pageNum, Integer pageSize);
|
||||
IPage<TransportCertRecordVO> pendingPage(String bizType, String bizKey,
|
||||
IPage<TransportCertRecord> page);
|
||||
|
||||
/**
|
||||
* 审核详情:同时返回待审快照与当前生效值,供前端做 diff
|
||||
@@ -69,12 +69,4 @@ public interface ICertAuditService {
|
||||
*/
|
||||
boolean audit(Long recordId, boolean pass, String rejectReason);
|
||||
|
||||
/**
|
||||
* 按主键取记录,供写指针时复用
|
||||
*
|
||||
* @param recordId 认证记录主键
|
||||
* @return 认证记录;不存在返回 null
|
||||
*/
|
||||
TransportCertRecord getRecordOrNull(Long recordId);
|
||||
|
||||
}
|
||||
|
||||
+4
-10
@@ -28,11 +28,11 @@ 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 com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
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.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.TransportCertRecord;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
@@ -71,7 +71,7 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
|
||||
@Override
|
||||
public IPage<TransportCertRecordVO> pendingPage(String bizType, String bizKey,
|
||||
Integer pageNum, Integer pageSize) {
|
||||
IPage<TransportCertRecord> page) {
|
||||
if (!CertificationRuleSupport.isValidBizType(bizType)) {
|
||||
throw new ServiceException("认证对象类型不正确");
|
||||
}
|
||||
@@ -80,9 +80,6 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
.eq(TransportCertRecord::getStatus, TransportCertRecord.STATUS_PENDING)
|
||||
.like(Func.isNotEmpty(bizKey), TransportCertRecord::getBizKey, bizKey)
|
||||
.orderByDesc(TransportCertRecord::getSubmitTime);
|
||||
Page<TransportCertRecord> page = Page.of(
|
||||
Func.isEmpty(pageNum) ? 1 : pageNum,
|
||||
Func.isEmpty(pageSize) ? 10 : pageSize);
|
||||
return TransportCertRecordWrapper.build().pageVO(certRecordService.page(page, queryWrapper));
|
||||
}
|
||||
|
||||
@@ -100,6 +97,8 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
detail.setStatusText(CertificationRuleSupport.statusText(record.getStatus()));
|
||||
detail.setSubmitTime(record.getSubmitTime());
|
||||
detail.setSubmitUser(record.getSubmitUser());
|
||||
// 面向前端的展示字段必须由服务端填好,否则审核页「提交人」一栏恒为空白
|
||||
detail.setSubmitUserName(UserCache.getUserRealName(record.getSubmitUser()));
|
||||
detail.setPendingSnapshotJson(record.getSnapshotJson());
|
||||
|
||||
// 生效值一侧:从未认证通过时为 null,前端按「无对比基准」处理
|
||||
@@ -148,11 +147,6 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportCertRecord getRecordOrNull(Long recordId) {
|
||||
return Func.isEmpty(recordId) ? null : certRecordService.getById(recordId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新认证对象的展示缓存,并在通过时把生效快照指针切到这条记录
|
||||
* <p>
|
||||
|
||||
+6
-12
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
@@ -73,18 +74,11 @@ public class DriverAppServiceImpl implements IDriverAppService {
|
||||
if (plates.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
// 精确匹配 + 规范化匹配(兼容库中带间隔符/横线的车牌)
|
||||
List<TransportVehicle> vehicles = transportVehicleService.list(Wrappers.<TransportVehicle>lambdaQuery()
|
||||
.and(w -> {
|
||||
w.in(TransportVehicle::getPlateNo, plates);
|
||||
for (String plate : plates) {
|
||||
w.or().apply(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(plate_no),'·',''),'•',''),'.',''),'-',''),' ','') = {0}",
|
||||
plate
|
||||
);
|
||||
}
|
||||
})
|
||||
.orderByDesc(TransportVehicle::getUpdateTime));
|
||||
// 精确 + 规范化双条件匹配,统一走共享实现(避免各调用点各自实现而漏掉写法差异)
|
||||
LambdaQueryWrapper<TransportVehicle> plateQuery = Wrappers.lambdaQuery();
|
||||
VehiclePlateSupport.applyPlateMatch(plateQuery, plates);
|
||||
List<TransportVehicle> vehicles = transportVehicleService.list(
|
||||
plateQuery.orderByDesc(TransportVehicle::getUpdateTime));
|
||||
// 去重(精确与规范化可能命中同一条)
|
||||
Map<Long, TransportVehicle> uniq = new LinkedHashMap<>();
|
||||
for (TransportVehicle vehicle : vehicles) {
|
||||
|
||||
+7
-5
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -263,11 +264,12 @@ public class DriverCertServiceImpl implements IDriverCertService {
|
||||
throw new ServiceException("车牌号不能为空");
|
||||
}
|
||||
String normalized = VehiclePlateSupport.normalize(plateNo);
|
||||
List<TransportVehicle> bound = transportVehicleService.list(
|
||||
Wrappers.<TransportVehicle>lambdaQuery()
|
||||
.in(TransportVehicle::getPlateNo,
|
||||
VehiclePlateSupport.splitPlates(driver.getDrivingVehicle())));
|
||||
return bound.stream()
|
||||
// 车牌软关联必须同时容忍写法差异:只精确 IN 会漏掉带间隔符/大小写不同的车牌,
|
||||
// 后置 filter 无法把漏掉的候选找回来(它只能收窄,不能放宽)
|
||||
LambdaQueryWrapper<TransportVehicle> plateQuery = Wrappers.lambdaQuery();
|
||||
VehiclePlateSupport.applyPlateMatch(plateQuery,
|
||||
VehiclePlateSupport.splitPlates(driver.getDrivingVehicle()));
|
||||
return transportVehicleService.list(plateQuery).stream()
|
||||
.filter(item -> normalized.equals(VehiclePlateSupport.normalize(item.getPlateNo())))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
+5
-3
@@ -391,9 +391,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
if (currentBoundPlates(driver).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<TransportVehicle> boundVehicles = transportVehicleService.list(
|
||||
Wrappers.<TransportVehicle>lambdaQuery()
|
||||
.in(TransportVehicle::getPlateNo, currentBoundPlates(driver)));
|
||||
// 用共享的车牌匹配条件:只做精确 IN 会漏掉库中带间隔符/大小写差异的车牌,
|
||||
// 那会把已认证的司机误判为未认证而拒绝接单
|
||||
LambdaQueryWrapper<TransportVehicle> plateQuery = Wrappers.lambdaQuery();
|
||||
VehiclePlateSupport.applyPlateMatch(plateQuery, currentBoundPlates(driver));
|
||||
List<TransportVehicle> boundVehicles = transportVehicleService.list(plateQuery);
|
||||
boolean anyCertified = boundVehicles.stream().anyMatch(vehicle ->
|
||||
certificationGate.isVehicleAvailable(
|
||||
vehicle.getStatus() != null && vehicle.getStatus() == BladeConstant.DB_STATUS_NORMAL,
|
||||
|
||||
+40
@@ -25,7 +25,9 @@
|
||||
*/
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -110,4 +112,42 @@ public final class VehiclePlateSupport {
|
||||
return splitPlates(drivingVehicle).contains(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按车牌匹配车辆的 SQL 条件:精确匹配 **或** 规范化后匹配
|
||||
* <p>
|
||||
* ⚠️ 只做精确匹配是不够的。{@link #splitPlates} 返回的已是**规范化**车牌
|
||||
* (去空格/间隔符/横线并转大写),若直接拿它与 {@code plate_no} 原始列做 IN,
|
||||
* 库中任何带间隔符或大小写差异的车牌都会**匹配不到**。
|
||||
* 后果不是「少查一条」,而是调用方据此判定「该司机名下没有可用车辆」——
|
||||
* 认证门槛会因此把**已认证的司机误判成未认证并拒绝接单**。
|
||||
* <p>
|
||||
* 库中已知存在这类写法差异({@code DriverAppServiceImpl.myVehicles()} 早就为此加了
|
||||
* 同样的兜底),故此处把规则收敛成一处,避免各调用点各自实现、各自漏掉。
|
||||
*
|
||||
* @param wrapper 查询条件
|
||||
* @param normalizedPlates 已规范化的车牌列表
|
||||
*/
|
||||
public static void applyPlateMatch(LambdaQueryWrapper<TransportVehicle> wrapper,
|
||||
List<String> normalizedPlates) {
|
||||
if (normalizedPlates == null || normalizedPlates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
wrapper.and(w -> {
|
||||
w.in(TransportVehicle::getPlateNo, normalizedPlates);
|
||||
for (String plate : normalizedPlates) {
|
||||
w.or().apply(NORMALIZED_PLATE_SQL + " = {0}", plate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 {@link #normalize} 等价的 SQL 表达式(列名固定为 {@code plate_no})
|
||||
* <p>
|
||||
* 两者必须保持一致:Java 侧剔除的字符集变了,这里也要跟着变。
|
||||
* {@code VehiclePlateSupportTest} 会断言两边覆盖同一批字符,防止悄悄分叉。
|
||||
*/
|
||||
public static final String NORMALIZED_PLATE_SQL =
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(plate_no)"
|
||||
+ ",'·',''),'•',''),'.',''),'.',''),'-',''),' ','')";
|
||||
|
||||
}
|
||||
|
||||
+51
@@ -1,5 +1,12 @@
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -20,6 +27,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
*/
|
||||
class VehiclePlateSupportTest {
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
// 构造 LambdaQueryWrapper 需要 MyBatis-Plus 的实体元数据缓存
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, TransportVehicle.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("单个车牌:原样返回规范化结果")
|
||||
void singlePlate() {
|
||||
@@ -88,4 +104,39 @@ class VehiclePlateSupportTest {
|
||||
assertFalse(VehiclePlateSupport.isBound("", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SQL 规范化表达式必须覆盖 Java 侧剔除的全部字符(否则两边会悄悄分叉)")
|
||||
void sqlNormalizationMirrorsJava() {
|
||||
String sql = VehiclePlateSupport.NORMALIZED_PLATE_SQL;
|
||||
// Java 的 normalize 会剔除这些排版字符并转大写,SQL 必须一一对应
|
||||
for (String noise : List.of("·", "•", ".", ".", "-", " ")) {
|
||||
assertTrue(sql.contains("'" + noise + "',''"),
|
||||
"SQL 规范化漏掉了字符 [" + noise + "],会导致该写法差异的车牌匹配不到");
|
||||
}
|
||||
assertTrue(sql.contains("UPPER(plate_no)"), "SQL 必须先转大写再比对");
|
||||
assertTrue(sql.contains("plate_no"), "表达式作用的列应为 plate_no");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("车牌匹配条件是「精确 IN 或 规范化相等」,不能只有精确 IN")
|
||||
void plateMatchIncludesNormalizedFallback() {
|
||||
LambdaQueryWrapper<TransportVehicle> wrapper = Wrappers.lambdaQuery();
|
||||
VehiclePlateSupport.applyPlateMatch(wrapper, List.of("桂A32523"));
|
||||
|
||||
String sql = wrapper.getTargetSql();
|
||||
assertTrue(sql.contains("plate_no IN"), "必须保留精确匹配,避免全表函数扫描");
|
||||
assertTrue(sql.contains("REPLACE"), "必须带规范化兜底,否则脏车牌匹配不到");
|
||||
assertTrue(sql.contains("OR"), "两者应是 OR 关系");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空车牌列表不产生任何条件(避免 IN () 语法错误)")
|
||||
void emptyPlateListAddsNoCondition() {
|
||||
LambdaQueryWrapper<TransportVehicle> wrapper = Wrappers.lambdaQuery();
|
||||
VehiclePlateSupport.applyPlateMatch(wrapper, List.of());
|
||||
assertEquals("", wrapper.getTargetSql().trim());
|
||||
VehiclePlateSupport.applyPlateMatch(wrapper, null);
|
||||
assertEquals("", wrapper.getTargetSql().trim());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user