From 63762b9dd0584ea13b36878520f6feef9294610a Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Fri, 2 Oct 2026 16:17:06 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=8F=B8=E6=9C=BA=E7=AB=AF?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E6=8F=90=E4=BA=A4=E4=B8=8E=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=EF=BC=88=E8=BD=A6=E8=BE=86?= =?UTF-8?q?/=E4=B8=AA=E4=BA=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现司机在小程序上提交认证、查看生效值与认证记录的能力。 提交链路的结构性保证(这是本次改造的核心约束「提交审核不影响车辆正常使用」): - 提交只产生一条新的待审记录,**绝不修改 effective_cert_record_id**, 因此已认证的车重新提交后依然可用;待审内容在审核通过前只存在于记录表里; - 同对象至多一条待审:落新记录前先把前一条待审转已作废, 且只动作待审——已通过/已驳回是终态,尤其不碰生效快照指着的那条; - 新记录状态显式设为待审:框架会把空 status 静默填成 1,而 1 在本表是「已通过」, 不显式设置会让一次提交凭空变成审核通过; - 新记录清空驳回原因,避免出现「审核中」却带着上一次驳回文案。 越权校验:司机只能提交/查看本人及本人绑定车辆的认证(复用 VehiclePlateSupport), 不可查看者返回业务异常而不是空列表——空列表会被误读成「没有记录」。 新增: - DriverAuthController(/driver/auth/**,兼容 /blade-transport/driver/auth/**) - IDriverCertService / DriverCertServiceImpl - PersonCertSubmitRequest / VehicleCertSubmitRequest - TransportCertRecordVO.statusText(状态文案统一在 Wrapper 翻译,四端一套说法) - IDriverAppService.currentDriverEntity()(认证写入需要司机实体) 17 个用例覆盖票 02 的三条验收点与不变式(含「重新提交后生效值仍是旧的」)。 全模块 33 个测试通过:mvn -o test -pl blade-service/blade-transport -am --- .../pojo/dto/PersonCertSubmitRequest.java | 90 +++++ .../pojo/dto/VehicleCertSubmitRequest.java | 87 +++++ .../pojo/vo/TransportCertRecordVO.java | 8 + .../controller/DriverAuthController.java | 121 ++++++ .../transport/service/IDriverAppService.java | 12 + .../transport/service/IDriverCertService.java | 99 +++++ .../service/impl/DriverAppServiceImpl.java | 5 + .../service/impl/DriverCertServiceImpl.java | 316 +++++++++++++++ .../wrapper/TransportCertRecordWrapper.java | 3 + .../impl/DriverCertServiceImplTest.java | 359 ++++++++++++++++++ 10 files changed, 1100 insertions(+) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PersonCertSubmitRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/VehicleCertSubmitRequest.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAuthController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverCertService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PersonCertSubmitRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PersonCertSubmitRequest.java new file mode 100644 index 00000000..29f20d5c --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PersonCertSubmitRequest.java @@ -0,0 +1,90 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDate; + +/** + * 司机端个人认证提交请求 + *

+ * 一次提交承载个人认证的**全部**内容(身份证 + 驾驶证 + 从业资格证), + * 三证一起提交、一起审核,不拆开。 + * + * @author Chill + */ +@Data +@Schema(description = "司机端个人认证提交请求") +public class PersonCertSubmitRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "真实姓名") + private String realName; + @Schema(description = "身份证号") + private String idCardNo; + @Schema(description = "身份证正面照") + private String idCardFrontUrl; + @Schema(description = "身份证反面照") + private String idCardBackUrl; + @Schema(description = "身份证有效期起") + private LocalDate idCardValidStart; + @Schema(description = "身份证有效期止") + private LocalDate idCardValidEnd; + @Schema(description = "身份证长期有效") + private Integer idCardLongTerm; + + @Schema(description = "驾驶证档案编号") + private String driverFileNo; + @Schema(description = "准驾车型") + private String driverClass; + @Schema(description = "驾驶证主页照片") + private String driverLicenseFrontUrl; + @Schema(description = "驾驶证副页照片") + private String driverLicenseBackUrl; + @Schema(description = "驾驶证有效期起") + private LocalDate driverValidStart; + @Schema(description = "驾驶证有效期止") + private LocalDate driverValidEnd; + @Schema(description = "驾驶证长期有效") + private Integer driverLongTerm; + + @Schema(description = "从业资格证号") + private String qualificationNo; + @Schema(description = "从业资格证照片") + private String qualificationUrl; + @Schema(description = "从业资格证有效期止") + private LocalDate qualificationValidEnd; + + @Schema(description = "大头照") + private String headPhoto; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/VehicleCertSubmitRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/VehicleCertSubmitRequest.java new file mode 100644 index 00000000..a05861a4 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/VehicleCertSubmitRequest.java @@ -0,0 +1,87 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDate; + +/** + * 司机端车辆认证提交请求 + *

+ * 一次提交承载该车辆认证的**全部**内容(行驶证 + 道路运输证),两证一起提交、一起审核。 + * 提交只产生新的待审记录,不覆盖车辆当前已生效的认证内容。 + * + * @author Chill + */ +@Data +@Schema(description = "司机端车辆认证提交请求") +public class VehicleCertSubmitRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "车辆ID:新增时为空,重新认证时必填") + private Long id; + @Schema(description = "车牌号") + private String plateNo; + @Schema(description = "车辆类型") + private String vehicleType; + @Schema(description = "车辆识别代号(车架号)") + private String vin; + @Schema(description = "发动机号") + private String engineNo; + @Schema(description = "车牌颜色") + private String plateColor; + + @Schema(description = "行驶证档案编号") + private String licenseNo; + @Schema(description = "行驶证主页照片") + private String licenseFrontUrl; + @Schema(description = "行驶证副页照片") + private String licenseBackUrl; + @Schema(description = "行驶证有效期起") + private LocalDate licenseValidStart; + @Schema(description = "行驶证有效期止") + private LocalDate licenseValidEnd; + @Schema(description = "行驶证长期有效") + private Integer licenseLongTerm; + + @Schema(description = "道路运输证号") + private String roadTransportNo; + @Schema(description = "道路运输证照片") + private String roadTransportUrl; + @Schema(description = "道路运输证有效期起") + private LocalDate roadTransportValidStart; + @Schema(description = "道路运输证有效期止") + private LocalDate roadTransportValidEnd; + @Schema(description = "道路运输证长期有效") + private Integer roadTransportLongTerm; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java index 8275d7be..ad12a17e 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java @@ -70,5 +70,13 @@ public class TransportCertRecordVO extends TransportCertRecord { @TableField(exist = false) @Schema(description = "审核人姓名") private String auditUserName; + /** + * 状态中文文案:审核中 / 已通过 / 已驳回 / 已作废 + *

+ * 由服务层统一翻译后下发,避免小程序、Web、接口三处各造一套说法。 + */ + @TableField(exist = false) + @Schema(description = "状态中文文案") + private String statusText; } 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 new file mode 100644 index 00000000..708e02ab --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAuthController.java @@ -0,0 +1,121 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.controller; + +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.PersonCertSubmitRequest; +import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest; +import org.springblade.transport.pojo.vo.TransportCertRecordVO; +import org.springblade.transport.service.IDriverCertService; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 司机端认证(小程序) + *

+ * 对外路径:{@code /api/blade-transport/driver/auth/**} + * 同时兼容未去前缀直连 {@code /blade-transport/driver/auth/**}。 + *

+ * 提交审核**不影响**该对象当前的可用性:待审内容只落认证记录, + * 审核通过前对外呈现的仍是旧的生效值。 + */ +@RestController +@AllArgsConstructor +@RequestMapping({"/driver/auth", "/blade-transport/driver/auth"}) +@Tag(name = "司机端认证", description = "小程序司机个人认证与车辆认证") +public class DriverAuthController extends BladeController { + + private final IDriverCertService driverCertService; + + // ------------------------------------------------------------ 个人认证 + + @PostMapping("/person") + @ApiOperationSupport(order = 1) + @Operation(summary = "提交个人认证", description = "三证(身份证+驾驶证+从业资格证)一起提交,生成待审记录") + public R submitPerson(@RequestBody PersonCertSubmitRequest request) { + return R.data(driverCertService.submitPersonCert(request)); + } + + @GetMapping("/person") + @ApiOperationSupport(order = 2) + @Operation(summary = "个人认证生效值", description = "返回当前生效快照的内容;从未认证通过时 data 为 null") + public R person() { + return R.data(driverCertService.getEffectivePersonCert()); + } + + // ------------------------------------------------------------ 车辆认证 + + @PostMapping("/vehicle") + @ApiOperationSupport(order = 3) + @Operation(summary = "提交车辆认证", description = "生成待审记录;不改变该车已生效的认证内容") + public R submitVehicle(@RequestBody VehicleCertSubmitRequest request) { + return R.data(driverCertService.submitVehicleCert(request)); + } + + @PutMapping("/vehicle") + @ApiOperationSupport(order = 4) + @Operation(summary = "重新认证车辆", description = "与提交同一语义,语义上用于已有车辆重新认证") + public R updateVehicle(@RequestBody VehicleCertSubmitRequest request) { + return R.data(driverCertService.submitVehicleCert(request)); + } + + @GetMapping("/vehicle/effective") + @ApiOperationSupport(order = 5) + @Operation(summary = "车辆认证生效值", description = "返回该车当前生效快照的内容") + public R vehicleEffective(@RequestParam Long vehicleId) { + return R.data(driverCertService.getEffectiveVehicleCert(vehicleId)); + } + + // ------------------------------------------------------------ 认证记录 + + @GetMapping("/record/list") + @ApiOperationSupport(order = 6) + @Operation(summary = "认证记录列表", description = "按提交时间倒序;只能查本人或本人绑定车辆的记录") + public R> recordList(@RequestParam String bizType, + @RequestParam Long bizId) { + return R.data(driverCertService.listRecords(bizType, bizId)); + } + + @GetMapping("/record/detail") + @ApiOperationSupport(order = 7) + @Operation(summary = "认证记录详情", description = "内容取自该次提交的完整快照") + public R recordDetail(@RequestParam Long id) { + return R.data(driverCertService.getRecord(id)); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java index 5f357a44..d7c7d34a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java @@ -22,6 +22,7 @@ */ package org.springblade.transport.service; +import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.pojo.vo.DriverVehicleCardVO; import org.springblade.transport.pojo.vo.DriverVO; @@ -44,4 +45,15 @@ public interface IDriverAppService { */ List myVehicles(); + /** + * 当前登录用户对应的司机**实体** + *

+ * 认证提交要写回司机表,且越权校验要读 {@code driving_vehicle}, + * 故这里返回实体而非视图对象。解析顺序:先按登录手机号匹配, + * 匹配不到再按 {@code user_id} 兜底。 + * + * @return 司机实体;未登录或查不到时返回 null + */ + Driver currentDriverEntity(); + } 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 new file mode 100644 index 00000000..f8b71437 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverCertService.java @@ -0,0 +1,99 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import org.springblade.transport.pojo.dto.PersonCertSubmitRequest; +import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest; +import org.springblade.transport.pojo.vo.TransportCertRecordVO; + +import java.util.List; + +/** + * 司机端认证服务(小程序) + *

+ * 认证对象是「一事一议」的:司机的个人认证与每一辆车的车辆认证各自独立成立、 + * 独立审核、独立留痕。本服务只负责司机端能做什么,管理端审核在审核服务里。 + * + * @author Chill + */ +public interface IDriverCertService { + + /** + * 提交个人认证(全量:身份证 + 驾驶证 + 从业资格证,三证一起审) + *

+ * 生成一条待审记录,并把本人前一条待审记录置为已作废。 + * **不修改生效快照**:已认证的司机在本次审核通过前仍按旧生效值认定。 + * + * @param request 个人认证内容 + * @return 新生成的认证记录主键 + */ + Long submitPersonCert(PersonCertSubmitRequest request); + + /** + * 读取本人当前生效的个人认证内容(供个人认证卡片展示) + * + * @return 生效快照内容;从未认证通过时返回 null + */ + PersonCertSubmitRequest getEffectivePersonCert(); + + /** + * 提交车辆认证(新增或重新认证) + *

+ * 生成一条待审记录,并把该车前一条待审记录置为已作废。 + * **不修改生效快照**:已认证的车辆在本次审核通过前仍按旧生效值认定, + * 因此提交审核不会影响车辆正常接单。 + * + * @param request 车辆认证内容 + * @return 新生成的认证记录主键 + */ + Long submitVehicleCert(VehicleCertSubmitRequest request); + + /** + * 读取某辆车当前生效的认证内容 + * + * @param vehicleId 车辆主键 + * @return 生效快照内容;从未认证通过时返回 null + */ + VehicleCertSubmitRequest getEffectiveVehicleCert(Long vehicleId); + + /** + * 查询某认证对象的认证记录列表(按提交时间倒序) + * + * @param bizType 认证对象类型:driver-司机,vehicle-车辆 + * @param bizId 认证对象主键 + * @return 认证记录列表 + */ + List listRecords(String bizType, Long bizId); + + /** + * 查询单条认证记录详情 + * + * @param recordId 认证记录主键 + * @return 认证记录;不存在或无权查看时抛业务异常 + */ + TransportCertRecordVO getRecord(Long recordId); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java index 759c86a4..84c8047a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java @@ -95,6 +95,11 @@ public class DriverAppServiceImpl implements IDriverAppService { return uniq.values().stream().map(this::toCard).collect(Collectors.toList()); } + @Override + public Driver currentDriverEntity() { + return currentDriver(null); + } + private Driver currentDriver(String mobileHint) { Long userId = AuthUtil.getUserId(); if (userId == null || userId <= 0) { 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 new file mode 100644 index 00000000..bf25d29c --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java @@ -0,0 +1,316 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service.impl; + +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.utils.Func; +import org.springblade.transport.pojo.dto.PersonCertSubmitRequest; +import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest; +import org.springblade.transport.pojo.entity.Driver; +import org.springblade.transport.pojo.entity.TransportCertRecord; +import org.springblade.transport.pojo.entity.TransportVehicle; +import org.springblade.transport.pojo.vo.TransportCertRecordVO; +import org.springblade.transport.service.IDriverAppService; +import org.springblade.transport.service.IDriverCertService; +import org.springblade.transport.service.IDriverService; +import org.springblade.transport.service.ITransportCertRecordService; +import org.springblade.transport.service.ITransportVehicleService; +import org.springblade.transport.support.CertificationRuleSupport; +import org.springblade.transport.support.VehiclePlateSupport; +import org.springblade.transport.wrapper.TransportCertRecordWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 司机端认证服务实现 + *

+ * 提交链路只做三件事:把本次提交的内容**原样**存成一条新的待审记录、 + * 把该对象前一条待审记录置为已作废、刷新展示缓存。 + *

+ * ⚠️ 提交**绝不**修改 {@code effective_cert_record_id},也**不覆盖**证件字段, + * 这正是「提交审核不影响车辆正常使用」的实现方式:待审内容只存在于记录表里, + * 在审核通过之前,该对象对外呈现的仍然是旧的生效值。 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class DriverCertServiceImpl implements IDriverCertService { + + private final IDriverAppService driverAppService; + private final IDriverService driverService; + private final ITransportVehicleService transportVehicleService; + private final ITransportCertRecordService transportCertRecordService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long submitPersonCert(PersonCertSubmitRequest request) { + if (Func.isEmpty(request)) { + throw new ServiceException("认证内容不能为空"); + } + Driver driver = requireCurrentDriver(); + String snapshotJson = JSON.toJSONString(request); + + // 先作废本人上一条待审,再落新记录:任何时刻同一对象至多一条待审。 + // 作废只动「待审」,已通过/已驳回都是终态,尤其不得碰生效快照指着的那条。 + supersedePending(TransportCertRecord.BIZ_TYPE_DRIVER, driver.getId()); + + TransportCertRecord record = newRecord( + TransportCertRecord.BIZ_TYPE_DRIVER, driver.getId(), + Func.toStr(driver.getDriverName(), ""), snapshotJson, driver.getId()); + transportCertRecordService.save(record); + + // 展示缓存:让小程序卡片显示「审核中」。可用性判定不看这个字段。 + updateDriverDisplayCache(driver.getId(), TransportCertRecord.STATUS_PENDING, null); + return record.getId(); + } + + @Override + public PersonCertSubmitRequest getEffectivePersonCert() { + Driver driver = requireCurrentDriver(); + // 当前司机实体已加载过,直接读它身上的指针,不再回查一次 + TransportCertRecord effective = findEffectiveRecord(driver.getEffectiveCertRecordId()); + return effective == null + ? null + : JSON.parseObject(effective.getSnapshotJson(), PersonCertSubmitRequest.class); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long submitVehicleCert(VehicleCertSubmitRequest request) { + if (Func.isEmpty(request)) { + throw new ServiceException("认证内容不能为空"); + } + Driver driver = requireCurrentDriver(); + TransportVehicle vehicle = resolveBoundVehicle(driver, request); + String snapshotJson = JSON.toJSONString(request); + + supersedePending(TransportCertRecord.BIZ_TYPE_VEHICLE, vehicle.getId()); + + TransportCertRecord record = newRecord( + TransportCertRecord.BIZ_TYPE_VEHICLE, vehicle.getId(), + Func.toStr(vehicle.getPlateNo(), ""), snapshotJson, driver.getId()); + transportCertRecordService.save(record); + + updateVehicleDisplayCache(vehicle.getId(), TransportCertRecord.STATUS_PENDING, null); + return record.getId(); + } + + @Override + public VehicleCertSubmitRequest getEffectiveVehicleCert(Long vehicleId) { + if (Func.isEmpty(vehicleId)) { + throw new ServiceException("车辆主键不能为空"); + } + TransportVehicle vehicle = transportVehicleService.getById(vehicleId); + if (vehicle == null) { + throw new ServiceException("车辆不存在"); + } + if (!VehiclePlateSupport.isBound(requireCurrentDriver().getDrivingVehicle(), vehicle.getPlateNo())) { + throw new ServiceException("无权查看该车辆的认证信息"); + } + TransportCertRecord effective = findEffectiveRecord(vehicle.getEffectiveCertRecordId()); + return effective == null + ? null + : JSON.parseObject(effective.getSnapshotJson(), VehicleCertSubmitRequest.class); + } + + @Override + public List listRecords(String bizType, Long bizId) { + requireValidBizType(bizType); + requireReadableTarget(bizType, bizId); + return transportCertRecordService.selectRecordList(bizType, bizId); + } + + @Override + public TransportCertRecordVO getRecord(Long recordId) { + TransportCertRecord record = transportCertRecordService.getById(recordId); + if (record == null) { + throw new ServiceException("认证记录不存在"); + } + requireReadableTarget(record.getBizType(), record.getBizId()); + return TransportCertRecordWrapper.build().entityVO(record); + } + + /** + * 把该认证对象当前那条待审记录置为已作废 + * + * @param bizType 认证对象类型 + * @param bizId 认证对象主键 + */ + private void supersedePending(String bizType, Long bizId) { + TransportCertRecord pending = transportCertRecordService.selectPendingRecord(bizType, bizId); + TransportCertRecord superseded = CertificationRuleSupport.supersedeIfPending(pending); + if (superseded != null) { + transportCertRecordService.updateById(superseded); + } + } + + /** + * 构造一条待审记录 + *

+ * 状态必须显式设置为待审:框架在插入时会把空的 {@code status} 静默填成 1, + * 而 1 在本表是「已通过」,那会让一次提交凭空变成审核通过。 + */ + private TransportCertRecord newRecord(String bizType, Long bizId, String bizKey, + String snapshotJson, Long submitUser) { + TransportCertRecord record = new TransportCertRecord(); + record.setBizType(bizType); + record.setBizId(bizId); + record.setBizKey(bizKey); + record.setSnapshotJson(snapshotJson); + record.setSubmitTime(LocalDateTime.now()); + record.setSubmitUser(submitUser); + CertificationRuleSupport.prepareForSubmit(record); + return record; + } + + /** + * 按生效快照指针取那条记录 + *

+ * 只按指针查,不看记录表里任何一条的「最新状态」——这正是生效值与待审值分离的意义。 + * + * @param effectiveRecordId 生效快照指针 + * @return 生效快照记录;指针为空时返回 null + */ + private TransportCertRecord findEffectiveRecord(Long effectiveRecordId) { + return Func.isEmpty(effectiveRecordId) + ? null + : transportCertRecordService.getById(effectiveRecordId); + } + + /** + * 刷新司机侧的「最新提交状态」展示缓存 + *

+ * 只写展示缓存,**不碰生效快照指针**。 + */ + private void updateDriverDisplayCache(Long driverId, Integer status, String rejectReason) { + Driver patch = new Driver(); + patch.setId(driverId); + patch.setCertificationStatus(status); + patch.setCertificationRejectReason(rejectReason); + driverService.updateById(patch); + } + + /** + * 刷新车辆侧的「最新提交状态」展示缓存 + *

+ * 只写展示缓存,**不碰生效快照指针**,也不覆盖车辆当前的证件字段。 + */ + private void updateVehicleDisplayCache(Long vehicleId, Integer status, String rejectReason) { + TransportVehicle patch = new TransportVehicle(); + patch.setId(vehicleId); + patch.setCertificationStatus(status); + patch.setCertificationRejectReason(rejectReason); + transportVehicleService.updateById(patch); + } + + /** + * 解析本次提交指向的车辆,并校验它确实绑定在当前司机名下 + * + * @param driver 当前司机 + * @param request 提交内容 + * @return 目标车辆 + */ + private TransportVehicle resolveBoundVehicle(Driver driver, VehicleCertSubmitRequest request) { + if (Func.isEmpty(driver.getDrivingVehicle())) { + throw new ServiceException("当前司机未绑定任何车辆"); + } + 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)) { + throw new ServiceException("车牌号不能为空"); + } + String normalized = VehiclePlateSupport.normalize(plateNo); + List bound = transportVehicleService.list( + Wrappers.lambdaQuery() + .in(TransportVehicle::getPlateNo, + VehiclePlateSupport.splitPlates(driver.getDrivingVehicle()))); + return bound.stream() + .filter(item -> normalized.equals(VehiclePlateSupport.normalize(item.getPlateNo()))) + .findFirst() + .orElse(null); + } + + /** + * 校验当前登录用户能否查看该认证对象的记录 + *

+ * 司机只能查自己的;车辆必须是本人绑定的。越权一律拒绝,不返回空列表 + * ——空列表会让调用方以为「没有记录」,而不是「无权查看」。 + */ + private void requireReadableTarget(String bizType, Long bizId) { + if (Func.isEmpty(bizId)) { + throw new ServiceException("认证对象主键不能为空"); + } + Driver driver = requireCurrentDriver(); + if (TransportCertRecord.BIZ_TYPE_DRIVER.equals(bizType)) { + if (!bizId.equals(driver.getId())) { + throw new ServiceException("无权查看其他司机的认证记录"); + } + return; + } + TransportVehicle vehicle = transportVehicleService.getById(bizId); + if (vehicle == null) { + throw new ServiceException("车辆不存在"); + } + if (!VehiclePlateSupport.isBound(driver.getDrivingVehicle(), vehicle.getPlateNo())) { + throw new ServiceException("无权查看该车辆的认证记录"); + } + } + + private void requireValidBizType(String bizType) { + if (!CertificationRuleSupport.isValidBizType(bizType)) { + throw new ServiceException("认证对象类型不正确"); + } + } + + private Driver requireCurrentDriver() { + Driver driver = driverAppService.currentDriverEntity(); + if (driver == null) { + throw new ServiceException("未找到当前登录用户对应的司机档案"); + } + return driver; + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java index d7e5171d..843a6a92 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java @@ -30,6 +30,7 @@ import org.springblade.core.tool.utils.BeanUtil; import org.springblade.system.cache.UserCache; import org.springblade.transport.pojo.entity.TransportCertRecord; import org.springblade.transport.pojo.vo.TransportCertRecordVO; +import org.springblade.transport.support.CertificationRuleSupport; import java.util.Objects; @@ -51,6 +52,8 @@ public class TransportCertRecordWrapper extends BaseEntityWrapper + * 重点守住票 02 的三条验收点与不变式: + * 提交不得改变生效快照、同对象至多一条待审、越权必须拒绝。 + */ +class DriverCertServiceImplTest { + + private static final Long DRIVER_ID = 9001L; + private static final Long VEHICLE_ID = 8001L; + private static final String PLATE = "桂A32523"; + + private IDriverAppService driverAppService; + private IDriverService driverService; + private ITransportVehicleService transportVehicleService; + private ITransportCertRecordService certRecordService; + private DriverCertServiceImpl service; + + @BeforeEach + void setUp() { + driverAppService = mock(IDriverAppService.class); + driverService = mock(IDriverService.class); + transportVehicleService = mock(ITransportVehicleService.class); + certRecordService = mock(ITransportCertRecordService.class); + service = new DriverCertServiceImpl(driverAppService, driverService, + transportVehicleService, certRecordService); + } + + // ---------------------------------------------------------------- 车辆提交 + + @Test + @DisplayName("车辆提交:落一条待审记录,且状态必须是待审(不能被框架默认值变成已通过)") + void submitVehicleCreatesPendingRecord() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, null)); + when(certRecordService.selectPendingRecord(TransportCertRecord.BIZ_TYPE_VEHICLE, VEHICLE_ID)) + .thenReturn(null); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setId(VEHICLE_ID); + request.setPlateNo(PLATE); + request.setVin("VIN123"); + + service.submitVehicleCert(request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TransportCertRecord.class); + verify(certRecordService).save(captor.capture()); + TransportCertRecord saved = captor.getValue(); + + assertEquals(TransportCertRecord.STATUS_PENDING, saved.getStatus()); + assertEquals(TransportCertRecord.BIZ_TYPE_VEHICLE, saved.getBizType()); + assertEquals(VEHICLE_ID, saved.getBizId()); + assertEquals(PLATE, saved.getBizKey()); + assertTrue(saved.getSnapshotJson().contains("VIN123"), "快照应承载本次提交的全量内容"); + assertNull(saved.getRejectReason(), "新提交不得继承上一条的驳回原因"); + } + + @Test + @DisplayName("车辆提交:不修改生效快照指针(提交不影响车辆可用性)") + void submitVehicleDoesNotTouchEffectivePointer() { + Long effectiveId = 777L; + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(transportVehicleService.getById(VEHICLE_ID)) + .thenReturn(vehicle(VEHICLE_ID, PLATE, effectiveId)); + when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setId(VEHICLE_ID); + service.submitVehicleCert(request); + + // 更新车辆时只能用展示缓存字段,且绝不能带上 effectiveCertRecordId + ArgumentCaptor captor = ArgumentCaptor.forClass(TransportVehicle.class); + verify(transportVehicleService).updateById(captor.capture()); + TransportVehicle updated = captor.getValue(); + assertNull(updated.getEffectiveCertRecordId(), + "提交链路不得写生效快照指针,否则「曾通过即永久可用」会被破坏"); + assertEquals(TransportCertRecord.STATUS_PENDING, updated.getCertificationStatus()); + } + + @Test + @DisplayName("车辆提交:前一条待审被置为已作废,不存在两条待审") + void submitVehicleSupersedesPreviousPending() { + TransportCertRecord previous = record(500L, TransportCertRecord.STATUS_PENDING); + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, null)); + when(certRecordService.selectPendingRecord(TransportCertRecord.BIZ_TYPE_VEHICLE, VEHICLE_ID)) + .thenReturn(previous); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setId(VEHICLE_ID); + service.submitVehicleCert(request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TransportCertRecord.class); + verify(certRecordService).updateById(captor.capture()); + assertEquals(TransportCertRecord.STATUS_SUPERSEDED, captor.getValue().getStatus()); + assertEquals(500L, captor.getValue().getId(), "被作废的应是原来那条待审"); + } + + @Test + @DisplayName("车辆提交:前一条已是终态时不得被改动(尤其不能碰已通过的那条)") + void submitVehicleLeavesTerminalRecordsAlone() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, 777L)); + // selectPendingRecord 只返回待审;已通过的不该被它返回 + when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setId(VEHICLE_ID); + service.submitVehicleCert(request); + + verify(certRecordService, never()).updateById(any(TransportCertRecord.class)); + } + + // ---------------------------------------------------------------- 越权 + + @Test + @DisplayName("越权:提交未绑定到自己名下的车辆必须拒绝") + void submitVehicleRejectsUnboundVehicle() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(transportVehicleService.getById(VEHICLE_ID)) + .thenReturn(vehicle(VEHICLE_ID, "桂B99999", null)); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setId(VEHICLE_ID); + + assertThrows(ServiceException.class, () -> service.submitVehicleCert(request)); + verify(certRecordService, never()).save(any(TransportCertRecord.class)); + } + + @Test + @DisplayName("越权:车牌写法差异不影响绑定判定(间隔符/大小写)") + void submitVehicleAcceptsBoundVehicleWithMessyPlate() { + // 司机档案里带间隔符,车辆表里是干净写法 + when(driverAppService.currentDriverEntity()).thenReturn(driver("桂A·32523")); + when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, "桂a32523", null)); + when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setId(VEHICLE_ID); + + service.submitVehicleCert(request); + verify(certRecordService).save(any(TransportCertRecord.class)); + } + + @Test + @DisplayName("越权:司机未绑定任何车辆时提交必须拒绝") + void submitVehicleRejectsWhenNoBoundVehicle() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(null)); + + VehicleCertSubmitRequest request = new VehicleCertSubmitRequest(); + request.setId(VEHICLE_ID); + + assertThrows(ServiceException.class, () -> service.submitVehicleCert(request)); + } + + @Test + @DisplayName("越权:不可查看他人车辆的认证记录列表") + void listRecordsRejectsOthersVehicle() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(transportVehicleService.getById(VEHICLE_ID)) + .thenReturn(vehicle(VEHICLE_ID, "桂B99999", null)); + + assertThrows(ServiceException.class, + () -> service.listRecords(TransportCertRecord.BIZ_TYPE_VEHICLE, VEHICLE_ID)); + } + + @Test + @DisplayName("越权:不可查看其他司机的认证记录列表") + void listRecordsRejectsOtherDriver() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + + assertThrows(ServiceException.class, + () -> service.listRecords(TransportCertRecord.BIZ_TYPE_DRIVER, 12345L)); + } + + // ---------------------------------------------------------------- 个人认证 + + @Test + @DisplayName("个人提交:落待审记录,且再提交时作废前一条待审") + void submitPersonCertSupersedes() { + TransportCertRecord previous = record(600L, TransportCertRecord.STATUS_PENDING); + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(certRecordService.selectPendingRecord(TransportCertRecord.BIZ_TYPE_DRIVER, DRIVER_ID)) + .thenReturn(previous); + + PersonCertSubmitRequest request = new PersonCertSubmitRequest(); + request.setRealName("张三"); + request.setIdCardNo("110101199001011234"); + + service.submitPersonCert(request); + + ArgumentCaptor saved = ArgumentCaptor.forClass(TransportCertRecord.class); + verify(certRecordService).save(saved.capture()); + assertEquals(TransportCertRecord.STATUS_PENDING, saved.getValue().getStatus()); + assertEquals(TransportCertRecord.BIZ_TYPE_DRIVER, saved.getValue().getBizType()); + assertEquals(DRIVER_ID, saved.getValue().getBizId()); + + ArgumentCaptor updated = ArgumentCaptor.forClass(TransportCertRecord.class); + verify(certRecordService).updateById(updated.capture()); + assertEquals(TransportCertRecord.STATUS_SUPERSEDED, updated.getValue().getStatus()); + } + + @Test + @DisplayName("个人提交:不写生效快照指针(只刷展示缓存)") + void submitPersonCertDoesNotTouchEffectivePointer() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null); + + PersonCertSubmitRequest request = new PersonCertSubmitRequest(); + request.setRealName("张三"); + service.submitPersonCert(request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Driver.class); + verify(driverService).updateById(captor.capture()); + assertNull(captor.getValue().getEffectiveCertRecordId(), + "提交链路不得写生效快照指针"); + } + + @Test + @DisplayName("个人提交:内容为空必须拒绝") + void submitPersonCertRejectsEmpty() { + assertThrows(ServiceException.class, () -> service.submitPersonCert(null)); + verify(certRecordService, never()).save(any(TransportCertRecord.class)); + } + + // ---------------------------------------------------------------- 生效值读取 + + @Test + @DisplayName("读生效值:有指针时返回指针指向的那条快照内容(不是最新提交)") + void getEffectivePersonCertReadsPointedRecord() { + Long effectiveId = 777L; + Driver driver = driver(PLATE); + driver.setEffectiveCertRecordId(effectiveId); + when(driverAppService.currentDriverEntity()).thenReturn(driver); + + PersonCertSubmitRequest effectiveContent = new PersonCertSubmitRequest(); + effectiveContent.setRealName("旧生效值"); + TransportCertRecord effective = record(effectiveId, TransportCertRecord.STATUS_APPROVED); + effective.setSnapshotJson(JSON.toJSONString(effectiveContent)); + when(certRecordService.getById(effectiveId)).thenReturn(effective); + + PersonCertSubmitRequest result = service.getEffectivePersonCert(); + + assertEquals("旧生效值", result.getRealName()); + } + + @Test + @DisplayName("读生效值:从未认证通过时返回 null(而不是抛错或返回最新提交)") + void getEffectivePersonCertReturnsNullWhenNeverCertified() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + + assertNull(service.getEffectivePersonCert()); + } + + @Test + @DisplayName("重新提交后,生效值仍是旧的(核心验收点:提交不影响已认证状态)") + void resubmitKeepsEffectiveValue() { + Long effectiveId = 777L; + Driver driver = driver(PLATE); + driver.setEffectiveCertRecordId(effectiveId); + when(driverAppService.currentDriverEntity()).thenReturn(driver); + when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null); + + PersonCertSubmitRequest oldContent = new PersonCertSubmitRequest(); + oldContent.setRealName("旧的已通过内容"); + TransportCertRecord effective = record(effectiveId, TransportCertRecord.STATUS_APPROVED); + effective.setSnapshotJson(JSON.toJSONString(oldContent)); + when(certRecordService.getById(effectiveId)).thenReturn(effective); + + // 提交新内容 + PersonCertSubmitRequest newContent = new PersonCertSubmitRequest(); + newContent.setRealName("新提交待审内容"); + service.submitPersonCert(newContent); + + // 生效值不受影响 + PersonCertSubmitRequest stillEffective = service.getEffectivePersonCert(); + assertEquals("旧的已通过内容", stillEffective.getRealName()); + assertNotEquals("新提交待审内容", stillEffective.getRealName()); + } + + @Test + @DisplayName("记录列表:类型非法必须拒绝") + void listRecordsRejectsInvalidBizType() { + assertThrows(ServiceException.class, () -> service.listRecords("truck", 1L)); + assertThrows(ServiceException.class, () -> service.listRecords(null, 1L)); + } + + @Test + @DisplayName("记录列表:本人司机记录按传入条件查询(走统一查询入口)") + void listRecordsForSelf() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(certRecordService.selectRecordList(TransportCertRecord.BIZ_TYPE_DRIVER, DRIVER_ID)) + .thenReturn(List.of()); + + assertTrue(service.listRecords(TransportCertRecord.BIZ_TYPE_DRIVER, DRIVER_ID).isEmpty()); + verify(certRecordService, times(1)) + .selectRecordList(TransportCertRecord.BIZ_TYPE_DRIVER, DRIVER_ID); + } + + // ---------------------------------------------------------------- 辅助 + + private Driver driver(String drivingVehicle) { + Driver driver = new Driver(); + driver.setId(DRIVER_ID); + driver.setDriverName("张三"); + driver.setDrivingVehicle(drivingVehicle); + return driver; + } + + private TransportVehicle vehicle(Long id, String plateNo, Long effectiveId) { + TransportVehicle vehicle = new TransportVehicle(); + vehicle.setId(id); + vehicle.setPlateNo(plateNo); + vehicle.setEffectiveCertRecordId(effectiveId); + return vehicle; + } + + private TransportCertRecord record(Long id, Integer status) { + TransportCertRecord record = new TransportCertRecord(); + record.setId(id); + record.setStatus(status); + record.setSnapshotJson("{}"); + return record; + } + +}