✨ 支持司机新增未登记车辆(原实现会让小程序「+ 新增车辆」必然失败)
小程序的车辆录入页允许司机直接填车牌提交,且空列表时提示 「暂无绑定车辆,点击「+ 新增车辆」添加」。但原实现只按「已完成绑定」查找车辆, 未登记的车牌一律抛「车辆不存在或未绑定」——而司机名下还没有这辆车,也就永远无从绑定, 形成死结:该入口必然提交失败(属票 07 的验收点)。 改为按车牌区分三种情形: - 未登记 → 就地建档(写入识别信息与证件内容)并把车牌追加到司机名下; - 已登记且属于本人 → 复用,不重复建档(容忍车牌写法差异); - 已登记但不在本人名下 → 拒绝,**不顺手改归属**(避免抢车)。 关键约束:新建车辆**不带生效快照指针**,故建档本身不会让一辆未审核通过的车变得可用 ——是否已认证仍只由审核通过决定。 4 个新用例覆盖上述三种情形与空车牌拒绝。全模块 89 个测试通过。
This commit is contained in:
+83
-22
@@ -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<TransportVehicle> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为司机新增一辆车并绑定到其名下
|
||||
* <p>
|
||||
* 只写入车辆的基本识别信息与本次提交的证件内容;车辆是否「已认证」仍由
|
||||
* 生效快照指针决定,故此处建档**不会**让一辆未审核通过的车变得可用。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前登录用户能否查看该认证对象的记录
|
||||
* <p>
|
||||
|
||||
+80
@@ -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<TransportVehicle> 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<Driver> 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<TransportCertRecord> 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) {
|
||||
|
||||
Reference in New Issue
Block a user