🐛 重新认证也要过内容校验,不再只校验新建车辆

起先 validateCertContent 只在 createVehicleForDriver 里调用,走 request.id 的重新认证路径
一句都不校验:待审内容虽然要等审核通过才写进车辆档案,但它已经是一条不可变记录,
并会在通过时按字段写回车辆行,于是「内容校验」对重新认证形同虚设。

- 内容校验提到 submitVehicleCert 里、resolveBoundVehicle 之前,两条路径共用;
- 「所属组织非空」拆成 requireOrganizationForNewVehicle,仍只在新车建档时要求:
  该列只在 INSERT 时需要,它属于内部管理归属,拿它拦住重新认证只会让司机无从下手。
- 补 2 个用例:重新认证传空「所有人」必须被拒;司机档案没有所属组织时重新认证仍应放行。
- 顺带把 7 个此前只传车牌/ID 的用例改用字段齐全的请求:否则它们会因为「车牌颜色不能为空」
  提前失败,看着还绿、其实已经不再验证原本要验证的越权与作废逻辑。
This commit is contained in:
2026-10-03 17:56:56 +08:00
parent 11e62c172c
commit ee135cd150
2 changed files with 74 additions and 28 deletions
@@ -90,19 +90,14 @@ public class DriverCertServiceImpl implements IDriverCertService {
/**
* 司机端提交的车辆认证内容校验
* <p>
* 服务端必须独立校验一遍,不能依赖小程序的前端校验:前端可绕过,
* 且这些字段里 {@code organization_name}、{@code approved_load_kg}、
* {@code business_relation}、{@code energy_type}、{@code registration_no}
* 都是 NOT NULL 且无默认值——缺一个这条 INSERT 就直接报数据库错误,
* 用户看到的是一串英文异常,而不是「哪个字段没填」。
* 服务端必须独立校验一遍,不能依赖小程序的前端校验:前端可绕过。
* 校验发生在**落快照之前**——待审内容虽然要等审核通过才写进车辆档案,但它已经是一条
* 不可变记录、并会在通过时按字段写回车辆行({@code applySnapshotToVehicle}),
* 校验只做在「新建车辆」那一条路上,重新认证就成了绕过校验的后门。
*
* @param driver 当前司机
* @param request 提交内容
*/
private void validateCertContent(Driver driver, VehicleCertSubmitRequest request) {
if (Func.isEmpty(driver.getOrganizationName())) {
throw new ServiceException("司机档案缺少所属组织,请联系管理员补充后再提交");
}
private void validateCertContent(VehicleCertSubmitRequest request) {
requireText(request.getPlateColor(), "车牌颜色不能为空", SHORT_TEXT_MAX_LENGTH);
requireText(request.getVehicleType(), "车辆类型不能为空", SHORT_TEXT_MAX_LENGTH);
requireText(request.getOwnerName(), "所有人不能为空", OWNER_NAME_MAX_LENGTH);
@@ -128,6 +123,21 @@ public class DriverCertServiceImpl implements IDriverCertService {
}
}
/**
* 建档前置条件:车辆行的 {@code organization_name} 是 NOT NULL 且无默认值,缺了这条 INSERT
* 直接报「Field 'organization_name' doesn't have a default value」,用户看到的是一串英文异常。
* <p>
* 只在**新建车辆**时要求:重新认证不会写这一列(它由管理端维护,属于内部管理归属),
* 拿它拦住重新认证只会让司机无从下手。
*
* @param driver 当前司机
*/
private void requireOrganizationForNewVehicle(Driver driver) {
if (Func.isEmpty(driver.getOrganizationName())) {
throw new ServiceException("司机档案缺少所属组织,请联系管理员补充后再提交");
}
}
/**
* 文本必填 + 长度校验
*
@@ -226,6 +236,9 @@ public class DriverCertServiceImpl implements IDriverCertService {
throw new ServiceException("认证内容不能为空");
}
Driver driver = requireCurrentDriver();
// 校验在 resolveBoundVehicle 之前:它既覆盖「新建车辆」,也覆盖「重新认证」——
// 后者原先一点校验都不做,内容全靠审核环节兜底
validateCertContent(request);
TransportVehicle vehicle = resolveBoundVehicle(driver, request);
String snapshotJson = JSON.toJSONString(request);
@@ -441,7 +454,8 @@ public class DriverCertServiceImpl implements IDriverCertService {
*/
private TransportVehicle createVehicleForDriver(Driver driver, VehicleCertSubmitRequest request,
String normalizedPlate) {
validateCertContent(driver, request);
// 内容校验已在 submitVehicleCert 里统一做过;这里只剩「这条 INSERT 需要什么」的前置条件
requireOrganizationForNewVehicle(driver);
TransportVehicle vehicle = new TransportVehicle();
vehicle.setPlateNo(normalizedPlate);
@@ -73,9 +73,8 @@ class DriverCertServiceImplTest {
when(certRecordService.selectPendingRecord(TransportCertRecord.BIZ_TYPE_VEHICLE, VEHICLE_ID))
.thenReturn(null);
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setId(VEHICLE_ID);
request.setPlateNo(PLATE);
request.setVin("VIN123");
service.submitVehicleCert(request);
@@ -99,9 +98,8 @@ class DriverCertServiceImplTest {
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, null));
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest vehicleRequest = new VehicleCertSubmitRequest();
VehicleCertSubmitRequest vehicleRequest = completeVehicleRequest(PLATE);
vehicleRequest.setId(VEHICLE_ID);
vehicleRequest.setPlateNo(PLATE);
service.submitVehicleCert(vehicleRequest);
PersonCertSubmitRequest personRequest = new PersonCertSubmitRequest();
@@ -129,7 +127,7 @@ class DriverCertServiceImplTest {
.thenReturn(vehicle(VEHICLE_ID, PLATE, effectiveId));
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setId(VEHICLE_ID);
service.submitVehicleCert(request);
@@ -151,7 +149,7 @@ class DriverCertServiceImplTest {
when(certRecordService.selectPendingRecord(TransportCertRecord.BIZ_TYPE_VEHICLE, VEHICLE_ID))
.thenReturn(previous);
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setId(VEHICLE_ID);
service.submitVehicleCert(request);
@@ -169,7 +167,7 @@ class DriverCertServiceImplTest {
// selectPendingRecord 只返回待审;已通过的不该被它返回
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setId(VEHICLE_ID);
service.submitVehicleCert(request);
@@ -185,7 +183,7 @@ class DriverCertServiceImplTest {
when(transportVehicleService.getById(VEHICLE_ID))
.thenReturn(vehicle(VEHICLE_ID, "桂B99999", null));
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setId(VEHICLE_ID);
assertThrows(ServiceException.class, () -> service.submitVehicleCert(request));
@@ -200,7 +198,7 @@ class DriverCertServiceImplTest {
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, "桂a32523", null));
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
VehicleCertSubmitRequest request = completeVehicleRequest("桂a32523");
request.setId(VEHICLE_ID);
service.submitVehicleCert(request);
@@ -212,7 +210,7 @@ class DriverCertServiceImplTest {
void submitVehicleRejectsWhenNoBoundVehicle() {
when(driverAppService.currentDriverEntity()).thenReturn(driver(null));
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setId(VEHICLE_ID);
assertThrows(ServiceException.class, () -> service.submitVehicleCert(request));
@@ -448,8 +446,7 @@ class DriverCertServiceImplTest {
when(transportVehicleService.list(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(vehicle(VEHICLE_ID, "桂A32523", null))); // 已登记,但不在本人名下
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
request.setPlateNo("桂A32523");
VehicleCertSubmitRequest request = completeVehicleRequest("桂A32523");
assertThrows(ServiceException.class, () -> service.submitVehicleCert(request));
verify(transportVehicleService, never()).save(any(TransportVehicle.class));
@@ -464,8 +461,7 @@ class DriverCertServiceImplTest {
.thenReturn(List.of(vehicle(VEHICLE_ID, "桂A·32523", null))); // 写法不同但同一辆
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
request.setPlateNo("桂a32523");
VehicleCertSubmitRequest request = completeVehicleRequest("桂a32523");
service.submitVehicleCert(request);
@@ -480,14 +476,50 @@ class DriverCertServiceImplTest {
void submitVehicleRejectsBlankPlate() {
when(driverAppService.currentDriverEntity()).thenReturn(driver("桂A32523"));
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
request.setPlateNo(" ");
// 内容给全,确保被拒的原因是车牌为空而不是别的字段(否则这条用例形同虚设)
VehicleCertSubmitRequest request = completeVehicleRequest(" ");
assertThrows(ServiceException.class, () -> service.submitVehicleCert(request));
ServiceException exception = assertThrows(ServiceException.class,
() -> service.submitVehicleCert(request));
assertTrue(exception.getMessage().contains("车牌"), exception.getMessage());
}
// ---------------------------------------------------------------- 辅助
@Test
@DisplayName("重新认证同样要过内容校验:只校验「新建车辆」等于给重新认证留了后门")
void submitVehicleValidatesContentOnResubmit() {
when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE));
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, null));
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setId(VEHICLE_ID);
request.setOwnerName("");
ServiceException exception = assertThrows(ServiceException.class,
() -> service.submitVehicleCert(request));
assertEquals("所有人不能为空", exception.getMessage());
verify(certRecordService, never()).save(any(TransportCertRecord.class));
}
@Test
@DisplayName("重新认证不要求司机档案有所属组织:那一列只在新建车辆时写,与管理归属无关")
void submitVehicleResubmitDoesNotRequireOrganization() {
Driver driver = driver(PLATE);
driver.setOrganizationName(null);
when(driverAppService.currentDriverEntity()).thenReturn(driver);
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, null));
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
VehicleCertSubmitRequest request = completeVehicleRequest(PLATE);
request.setId(VEHICLE_ID);
service.submitVehicleCert(request);
verify(certRecordService).save(any(TransportCertRecord.class));
}
@Test
@DisplayName("司机建车:字段不齐时给可读提示,而不是让数据库报 NOT NULL")
void submitVehicleRejectsIncompleteRequest() {