✨ 补车辆认证删除接口(契约与小程序都已在调用,后端却缺失)

api-contract.md 承诺 DELETE /driver/auth/vehicle/{id},小程序
api/auth.ts 的 deleteVehicleAuth 也从 subpackages/auth/vehicle.vue 真实调用它,
但后端一直没实现——该操作此前必然 404。

实现要点:
- 对车辆做**逻辑删除**(deleteLogic,is_deleted=1),认证记录**一律保留**:
  记录是不可变快照,也是「曾通过即永久可用」这一判定的依据,物理删除会让历史无从追溯;
- 越权校验复用同一套车牌绑定判定,写法差异(间隔符/大小写)不影响结果。

3 个用例:逻辑删除而非物理删除(显式断言从不调用 removeById/removeIds)、
越权拒绝、脏车牌仍可识别。全模块 85 个测试通过。
This commit is contained in:
2026-10-02 17:32:07 +08:00
parent 4de1ea5209
commit c66af4c162
4 changed files with 80 additions and 0 deletions
@@ -40,9 +40,11 @@ import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
import org.springblade.transport.service.IDriverCertService;
import org.springblade.transport.support.OcrTypeMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -100,6 +102,13 @@ public class DriverAuthController extends BladeController {
return R.data(driverCertService.submitVehicleCert(request));
}
@DeleteMapping("/vehicle/{id}")
@ApiOperationSupport(order = 5)
@Operation(summary = "删除车辆认证", description = "对车辆做逻辑删除;认证记录保留,不物理删除")
public R<Boolean> deleteVehicle(@PathVariable Long id) {
return R.status(driverCertService.deleteVehicleCert(id));
}
@GetMapping("/vehicle/effective")
@ApiOperationSupport(order = 5)
@Operation(summary = "车辆认证生效值", description = "返回该车当前生效快照的内容")
@@ -71,6 +71,18 @@ public interface IDriverCertService {
*/
Long submitVehicleCert(VehicleCertSubmitRequest request);
/**
* 删除车辆认证(按逻辑删除处理)
* <p>
* 只把车辆标记为已删除,**认证记录一律保留**——记录是不可变快照,
* 也是「曾通过即永久可用」这一判定的依据,物理删除会让历史无从追溯。
* 只能删除本人绑定的车辆,越权拒绝。
*
* @param vehicleId 车辆主键
* @return 是否成功
*/
boolean deleteVehicleCert(Long vehicleId);
/**
* 读取某辆车当前生效的认证内容
*
@@ -49,6 +49,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@@ -127,6 +128,25 @@ public class DriverCertServiceImpl implements IDriverCertService {
return record.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteVehicleCert(Long vehicleId) {
if (Func.isEmpty(vehicleId)) {
throw new ServiceException("车辆主键不能为空");
}
Driver driver = requireCurrentDriver();
TransportVehicle vehicle = transportVehicleService.getById(vehicleId);
if (vehicle == null) {
throw new ServiceException("车辆不存在");
}
// 越权校验:与提交链路同一套绑定判定
if (!VehiclePlateSupport.isBound(driver.getDrivingVehicle(), vehicle.getPlateNo())) {
throw new ServiceException("无权删除该车辆的认证");
}
// 逻辑删除车辆;认证记录**不删**——它是不可变快照,且是「曾通过即永久可用」的依据
return transportVehicleService.deleteLogic(Collections.singletonList(vehicleId));
}
@Override
public VehicleCertSubmitRequest getEffectiveVehicleCert(Long vehicleId) {
if (Func.isEmpty(vehicleId)) {
@@ -330,6 +330,45 @@ class DriverCertServiceImplTest {
.selectRecordList(TransportCertRecord.BIZ_TYPE_DRIVER, DRIVER_ID);
}
// ---------------------------------------------------------------- 删除车辆认证
@Test
@DisplayName("删除车辆认证:逻辑删除车辆,且**不删**认证记录")
void deleteVehicleCertIsLogicalOnly() {
when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE));
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, 777L));
service.deleteVehicleCert(VEHICLE_ID);
// 走的是 deleteLogic(is_deleted=1),不是物理删除
verify(transportVehicleService).deleteLogic(java.util.Collections.singletonList(VEHICLE_ID));
verify(transportVehicleService, never()).removeById(anyLong());
// 认证记录是历史依据,任何情况下都不得被删
verify(certRecordService, never()).removeById(anyLong());
verify(certRecordService, never()).removeByIds(any());
}
@Test
@DisplayName("删除车辆认证:越权必须拒绝")
void deleteVehicleCertRejectsUnbound() {
when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE));
when(transportVehicleService.getById(VEHICLE_ID))
.thenReturn(vehicle(VEHICLE_ID, "桂B99999", null));
assertThrows(ServiceException.class, () -> service.deleteVehicleCert(VEHICLE_ID));
verify(transportVehicleService, never()).deleteLogic(any());
}
@Test
@DisplayName("删除车辆认证:车牌写法差异不影响绑定判定")
void deleteVehicleCertAcceptsMessyPlate() {
when(driverAppService.currentDriverEntity()).thenReturn(driver("桂A·32523"));
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, "桂a32523", null));
service.deleteVehicleCert(VEHICLE_ID);
verify(transportVehicleService).deleteLogic(java.util.Collections.singletonList(VEHICLE_ID));
}
// ---------------------------------------------------------------- 辅助
private Driver driver(String drivingVehicle) {