From fbced4e6a09be74e0285d3d3a9ed06001521fc0d Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Fri, 2 Oct 2026 17:35:33 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=94=AF=E6=8C=81=E5=8F=B8?= =?UTF-8?q?=E6=9C=BA=E6=96=B0=E5=A2=9E=E6=9C=AA=E7=99=BB=E8=AE=B0=E8=BD=A6?= =?UTF-8?q?=E8=BE=86=EF=BC=88=E5=8E=9F=E5=AE=9E=E7=8E=B0=E4=BC=9A=E8=AE=A9?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E3=80=8C+=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=BD=A6=E8=BE=86=E3=80=8D=E5=BF=85=E7=84=B6=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 小程序的车辆录入页允许司机直接填车牌提交,且空列表时提示 「暂无绑定车辆,点击「+ 新增车辆」添加」。但原实现只按「已完成绑定」查找车辆, 未登记的车牌一律抛「车辆不存在或未绑定」——而司机名下还没有这辆车,也就永远无从绑定, 形成死结:该入口必然提交失败(属票 07 的验收点)。 改为按车牌区分三种情形: - 未登记 → 就地建档(写入识别信息与证件内容)并把车牌追加到司机名下; - 已登记且属于本人 → 复用,不重复建档(容忍车牌写法差异); - 已登记但不在本人名下 → 拒绝,**不顺手改归属**(避免抢车)。 关键约束:新建车辆**不带生效快照指针**,故建档本身不会让一辆未审核通过的车变得可用 ——是否已认证仍只由审核通过决定。 4 个新用例覆盖上述三种情形与空车牌拒绝。全模块 89 个测试通过。 --- .../service/impl/DriverCertServiceImpl.java | 105 ++++++++++++++---- .../impl/DriverCertServiceImplTest.java | 80 +++++++++++++ 2 files changed, 163 insertions(+), 22 deletions(-) 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 d23127f2..53d1e811 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 @@ -30,6 +30,7 @@ import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.constant.BladeConstant; import org.springblade.core.tool.utils.Func; import org.springblade.transport.pojo.dto.PersonCertSubmitRequest; import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest; @@ -263,38 +264,98 @@ public class DriverCertServiceImpl implements IDriverCertService { * @return 目标车辆 */ private TransportVehicle resolveBoundVehicle(Driver driver, VehicleCertSubmitRequest request) { - if (Func.isEmpty(driver.getDrivingVehicle())) { - throw new ServiceException("当前司机未绑定任何车辆"); + // 按 id 定位:只能操作本人绑定的车,否则越权 + if (Func.isNotEmpty(request.getId())) { + TransportVehicle vehicle = transportVehicleService.getById(request.getId()); + if (vehicle == null) { + throw new ServiceException("车辆不存在"); + } + if (!VehiclePlateSupport.isBound(driver.getDrivingVehicle(), vehicle.getPlateNo())) { + throw new ServiceException("无权操作该车辆的认证"); + } + return vehicle; } - TransportVehicle vehicle = Func.isEmpty(request.getId()) - ? findByPlateWithinBound(driver, request.getPlateNo()) - : transportVehicleService.getById(request.getId()); - if (vehicle == null) { - throw new ServiceException("车辆不存在或未绑定到当前司机"); - } - // 越权校验:无论按 id 还是按车牌取到,最后都核对一次绑定关系 - if (!VehiclePlateSupport.isBound(driver.getDrivingVehicle(), vehicle.getPlateNo())) { - throw new ServiceException("无权操作该车辆的认证"); - } - return vehicle; - } - private TransportVehicle findByPlateWithinBound(Driver driver, String plateNo) { - if (Func.isEmpty(plateNo)) { + String normalized = VehiclePlateSupport.normalize(request.getPlateNo()); + if (Func.isEmpty(normalized)) { throw new ServiceException("车牌号不能为空"); } - String normalized = VehiclePlateSupport.normalize(plateNo); - // 车牌软关联必须同时容忍写法差异:只精确 IN 会漏掉带间隔符/大小写不同的车牌, - // 后置 filter 无法把漏掉的候选找回来(它只能收窄,不能放宽) + + // 按车牌定位:已登记的车必须已绑定到本人;未登记的车视为「新增」,就地建档并绑定。 + // 小程序「+ 新增车辆」允许司机直接填车牌,若这里只认已有绑定, + // 新车的提交必然失败——而司机名下还没有这辆车,也就永远无从绑定。 + TransportVehicle existing = findVehicleByAnyPlateForm(normalized); + if (existing != null) { + if (existing.getIsDeleted() != null && existing.getIsDeleted() != 0) { + throw new ServiceException("该车牌已被删除,请联系管理员"); + } + if (!VehiclePlateSupport.isBound(driver.getDrivingVehicle(), existing.getPlateNo())) { + // 已登记但不在本人名下,可能属于其他司机 —— 拒绝,不能顺手改归属 + throw new ServiceException("该车牌已归属其他司机,请联系管理员"); + } + return existing; + } + return createVehicleForDriver(driver, request, normalized); + } + + /** + * 按车牌(容忍写法差异)查已登记的车辆 + * + * @param normalizedPlate 已规范化的车牌 + * @return 车辆;未登记返回 null + */ + private TransportVehicle findVehicleByAnyPlateForm(String normalizedPlate) { LambdaQueryWrapper plateQuery = Wrappers.lambdaQuery(); - VehiclePlateSupport.applyPlateMatch(plateQuery, - VehiclePlateSupport.splitPlates(driver.getDrivingVehicle())); + VehiclePlateSupport.applyPlateMatch(plateQuery, List.of(normalizedPlate)); return transportVehicleService.list(plateQuery).stream() - .filter(item -> normalized.equals(VehiclePlateSupport.normalize(item.getPlateNo()))) + .filter(item -> normalizedPlate.equals(VehiclePlateSupport.normalize(item.getPlateNo()))) .findFirst() .orElse(null); } + /** + * 为司机新增一辆车并绑定到其名下 + *

+ * 只写入车辆的基本识别信息与本次提交的证件内容;车辆是否「已认证」仍由 + * 生效快照指针决定,故此处建档**不会**让一辆未审核通过的车变得可用。 + * + * @param driver 当前司机 + * @param request 提交内容 + * @param normalizedPlate 已规范化的车牌 + * @return 新建的车辆 + */ + private TransportVehicle createVehicleForDriver(Driver driver, VehicleCertSubmitRequest request, + String normalizedPlate) { + TransportVehicle vehicle = new TransportVehicle(); + vehicle.setPlateNo(normalizedPlate); + vehicle.setPlateColor(request.getPlateColor()); + vehicle.setVehicleType(request.getVehicleType()); + vehicle.setDrivingLicenseNo(request.getLicenseNo()); + vehicle.setDrivingLicenseStartDate(request.getLicenseValidStart()); + vehicle.setDrivingLicenseEndDate(request.getLicenseValidEnd()); + vehicle.setDrivingLicenseLongTerm(request.getLicenseLongTerm()); + vehicle.setDrivingLicenseImage(request.getLicenseFrontUrl()); + vehicle.setDrivingLicenseMainBack(request.getLicenseBackUrl()); + vehicle.setRoadTransportCertNo(request.getRoadTransportNo()); + vehicle.setRoadTransportCertStartDate(request.getRoadTransportValidStart()); + vehicle.setRoadTransportCertEndDate(request.getRoadTransportValidEnd()); + vehicle.setRoadTransportCertLongTerm(request.getRoadTransportLongTerm()); + vehicle.setRoadTransportCertImage(request.getRoadTransportUrl()); + vehicle.setStatus(BladeConstant.DB_STATUS_NORMAL); + transportVehicleService.save(vehicle); + + // 绑定到司机名下,否则本人后续仍无法操作这辆车 + String drivingVehicle = Func.isEmpty(driver.getDrivingVehicle()) + ? normalizedPlate + : driver.getDrivingVehicle() + "," + normalizedPlate; + Driver patch = new Driver(); + patch.setId(driver.getId()); + patch.setDrivingVehicle(drivingVehicle); + driverService.updateById(patch); + driver.setDrivingVehicle(drivingVehicle); + return vehicle; + } + /** * 校验当前登录用户能否查看该认证对象的记录 *

diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java index 3b715c05..0f8dcf7a 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java @@ -1,5 +1,6 @@ package org.springblade.transport.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.alibaba.fastjson2.JSON; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -369,6 +370,85 @@ class DriverCertServiceImplTest { verify(transportVehicleService).deleteLogic(java.util.Collections.singletonList(VEHICLE_ID)); } + // ---------------------------------------------------------------- 新增车辆 + + @Test + @DisplayName("新增车辆:车牌未登记时就地建档并绑定到本人,且新车不被视为已认证") + void submitVehicleCreatesUnregisteredVehicle() { + Driver driver = driver(null); // 司机名下还没有任何车 + when(driverAppService.currentDriverEntity()).thenReturn(driver); + // 全库都查不到这个车牌 + when(transportVehicleService.list(any(LambdaQueryWrapper.class))).thenReturn(List.of()); + when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setPlateNo("桂A·32523"); // 带间隔符,应被规范化 + request.setVehicleType("重型货车"); + + service.submitVehicleCert(request); + + // 建档:保存了一辆车,车挂牌照为规范化结果,且状态为启用 + ArgumentCaptor vehicleCaptor = ArgumentCaptor.forClass(TransportVehicle.class); + verify(transportVehicleService).save(vehicleCaptor.capture()); + assertEquals("桂A32523", vehicleCaptor.getValue().getPlateNo()); + assertEquals("重型货车", vehicleCaptor.getValue().getVehicleType()); + assertNull(vehicleCaptor.getValue().getEffectiveCertRecordId(), + "新建的车不得自带生效快照——是否已认证只能由审核通过决定"); + + // 绑定:司机的 driving_vehicle 被追加该车牌 + ArgumentCaptor driverCaptor = ArgumentCaptor.forClass(Driver.class); + verify(driverService).updateById(driverCaptor.capture()); + assertEquals("桂A32523", driverCaptor.getValue().getDrivingVehicle()); + + // 仍然产生一条待审记录 + verify(certRecordService).save(any(TransportCertRecord.class)); + } + + @Test + @DisplayName("新增车辆:车牌已登记在别人名下必须拒绝,不能顺手改归属") + void submitVehicleRejectsPlateOwnedByAnotherDriver() { + when(driverAppService.currentDriverEntity()).thenReturn(driver("桂B11111")); + when(transportVehicleService.list(any(LambdaQueryWrapper.class))) + .thenReturn(List.of(vehicle(VEHICLE_ID, "桂A32523", null))); // 已登记,但不在本人名下 + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setPlateNo("桂A32523"); + + assertThrows(ServiceException.class, () -> service.submitVehicleCert(request)); + verify(transportVehicleService, never()).save(any(TransportVehicle.class)); + verify(certRecordService, never()).save(any(TransportCertRecord.class)); + } + + @Test + @DisplayName("新增车辆:已登记且属于本人时直接复用,不重复建档") + void submitVehicleReusesOwnRegisteredVehicle() { + when(driverAppService.currentDriverEntity()).thenReturn(driver("桂A32523")); + when(transportVehicleService.list(any(LambdaQueryWrapper.class))) + .thenReturn(List.of(vehicle(VEHICLE_ID, "桂A·32523", null))); // 写法不同但同一辆 + when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setPlateNo("桂a32523"); + + service.submitVehicleCert(request); + + verify(transportVehicleService, never()).save(any(TransportVehicle.class)); + ArgumentCaptor captor = ArgumentCaptor.forClass(TransportCertRecord.class); + verify(certRecordService).save(captor.capture()); + assertEquals(VEHICLE_ID, captor.getValue().getBizId(), "应挂到既有车辆上"); + } + + @Test + @DisplayName("新增车辆:车牌为空必须拒绝") + void submitVehicleRejectsBlankPlate() { + when(driverAppService.currentDriverEntity()).thenReturn(driver("桂A32523")); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setPlateNo(" "); + + assertThrows(ServiceException.class, () -> service.submitVehicleCert(request)); + } + // ---------------------------------------------------------------- 辅助 private Driver driver(String drivingVehicle) {