From c66af4c162a7b0657d6acb9d25185ba81671d294 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Fri, 2 Oct 2026 17:32:07 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E8=A1=A5=E8=BD=A6=E8=BE=86?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E5=88=A0=E9=99=A4=E6=8E=A5=E5=8F=A3=EF=BC=88?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E4=B8=8E=E5=B0=8F=E7=A8=8B=E5=BA=8F=E9=83=BD?= =?UTF-8?q?=E5=B7=B2=E5=9C=A8=E8=B0=83=E7=94=A8=EF=BC=8C=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E5=8D=B4=E7=BC=BA=E5=A4=B1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 个测试通过。 --- .../controller/DriverAuthController.java | 9 +++++ .../transport/service/IDriverCertService.java | 12 ++++++ .../service/impl/DriverCertServiceImpl.java | 20 ++++++++++ .../impl/DriverCertServiceImplTest.java | 39 +++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAuthController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAuthController.java index 9d8fb576..6f7e7876 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAuthController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAuthController.java @@ -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 deleteVehicle(@PathVariable Long id) { + return R.status(driverCertService.deleteVehicleCert(id)); + } + @GetMapping("/vehicle/effective") @ApiOperationSupport(order = 5) @Operation(summary = "车辆认证生效值", description = "返回该车当前生效快照的内容") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverCertService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverCertService.java index f8b71437..7a76e5ff 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverCertService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverCertService.java @@ -71,6 +71,18 @@ public interface IDriverCertService { */ Long submitVehicleCert(VehicleCertSubmitRequest request); + /** + * 删除车辆认证(按逻辑删除处理) + *

+ * 只把车辆标记为已删除,**认证记录一律保留**——记录是不可变快照, + * 也是「曾通过即永久可用」这一判定的依据,物理删除会让历史无从追溯。 + * 只能删除本人绑定的车辆,越权拒绝。 + * + * @param vehicleId 车辆主键 + * @return 是否成功 + */ + boolean deleteVehicleCert(Long vehicleId); + /** * 读取某辆车当前生效的认证内容 * 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 072eafc9..d23127f2 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 @@ -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)) { 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 9a658c88..3b715c05 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 @@ -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) {