✨ 司机端认证提交与记录查询接口(车辆/个人)
实现司机在小程序上提交认证、查看生效值与认证记录的能力。 提交链路的结构性保证(这是本次改造的核心约束「提交审核不影响车辆正常使用」): - 提交只产生一条新的待审记录,**绝不修改 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
This commit is contained in:
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 司机端个人认证提交请求
|
||||
* <p>
|
||||
* 一次提交承载个人认证的**全部**内容(身份证 + 驾驶证 + 从业资格证),
|
||||
* 三证一起提交、一起审核,不拆开。
|
||||
*
|
||||
* @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;
|
||||
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 司机端车辆认证提交请求
|
||||
* <p>
|
||||
* 一次提交承载该车辆认证的**全部**内容(行驶证 + 道路运输证),两证一起提交、一起审核。
|
||||
* 提交只产生新的待审记录,不覆盖车辆当前已生效的认证内容。
|
||||
*
|
||||
* @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;
|
||||
|
||||
}
|
||||
+8
@@ -70,5 +70,13 @@ public class TransportCertRecordVO extends TransportCertRecord {
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "审核人姓名")
|
||||
private String auditUserName;
|
||||
/**
|
||||
* 状态中文文案:审核中 / 已通过 / 已驳回 / 已作废
|
||||
* <p>
|
||||
* 由服务层统一翻译后下发,避免小程序、Web、接口三处各造一套说法。
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "状态中文文案")
|
||||
private String statusText;
|
||||
|
||||
}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 司机端认证(小程序)
|
||||
* <p>
|
||||
* 对外路径:{@code /api/blade-transport/driver/auth/**}
|
||||
* 同时兼容未去前缀直连 {@code /blade-transport/driver/auth/**}。
|
||||
* <p>
|
||||
* 提交审核**不影响**该对象当前的可用性:待审内容只落认证记录,
|
||||
* 审核通过前对外呈现的仍是旧的生效值。
|
||||
*/
|
||||
@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<Long> submitPerson(@RequestBody PersonCertSubmitRequest request) {
|
||||
return R.data(driverCertService.submitPersonCert(request));
|
||||
}
|
||||
|
||||
@GetMapping("/person")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "个人认证生效值", description = "返回当前生效快照的内容;从未认证通过时 data 为 null")
|
||||
public R<PersonCertSubmitRequest> person() {
|
||||
return R.data(driverCertService.getEffectivePersonCert());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 车辆认证
|
||||
|
||||
@PostMapping("/vehicle")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "提交车辆认证", description = "生成待审记录;不改变该车已生效的认证内容")
|
||||
public R<Long> submitVehicle(@RequestBody VehicleCertSubmitRequest request) {
|
||||
return R.data(driverCertService.submitVehicleCert(request));
|
||||
}
|
||||
|
||||
@PutMapping("/vehicle")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "重新认证车辆", description = "与提交同一语义,语义上用于已有车辆重新认证")
|
||||
public R<Long> updateVehicle(@RequestBody VehicleCertSubmitRequest request) {
|
||||
return R.data(driverCertService.submitVehicleCert(request));
|
||||
}
|
||||
|
||||
@GetMapping("/vehicle/effective")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "车辆认证生效值", description = "返回该车当前生效快照的内容")
|
||||
public R<VehicleCertSubmitRequest> vehicleEffective(@RequestParam Long vehicleId) {
|
||||
return R.data(driverCertService.getEffectiveVehicleCert(vehicleId));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 认证记录
|
||||
|
||||
@GetMapping("/record/list")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "认证记录列表", description = "按提交时间倒序;只能查本人或本人绑定车辆的记录")
|
||||
public R<List<TransportCertRecordVO>> 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<TransportCertRecordVO> recordDetail(@RequestParam Long id) {
|
||||
return R.data(driverCertService.getRecord(id));
|
||||
}
|
||||
|
||||
}
|
||||
+12
@@ -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<DriverVehicleCardVO> myVehicles();
|
||||
|
||||
/**
|
||||
* 当前登录用户对应的司机**实体**
|
||||
* <p>
|
||||
* 认证提交要写回司机表,且越权校验要读 {@code driving_vehicle},
|
||||
* 故这里返回实体而非视图对象。解析顺序:先按登录手机号匹配,
|
||||
* 匹配不到再按 {@code user_id} 兜底。
|
||||
*
|
||||
* @return 司机实体;未登录或查不到时返回 null
|
||||
*/
|
||||
Driver currentDriverEntity();
|
||||
|
||||
}
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 司机端认证服务(小程序)
|
||||
* <p>
|
||||
* 认证对象是「一事一议」的:司机的个人认证与每一辆车的车辆认证各自独立成立、
|
||||
* 独立审核、独立留痕。本服务只负责司机端能做什么,管理端审核在审核服务里。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface IDriverCertService {
|
||||
|
||||
/**
|
||||
* 提交个人认证(全量:身份证 + 驾驶证 + 从业资格证,三证一起审)
|
||||
* <p>
|
||||
* 生成一条待审记录,并把本人前一条待审记录置为已作废。
|
||||
* **不修改生效快照**:已认证的司机在本次审核通过前仍按旧生效值认定。
|
||||
*
|
||||
* @param request 个人认证内容
|
||||
* @return 新生成的认证记录主键
|
||||
*/
|
||||
Long submitPersonCert(PersonCertSubmitRequest request);
|
||||
|
||||
/**
|
||||
* 读取本人当前生效的个人认证内容(供个人认证卡片展示)
|
||||
*
|
||||
* @return 生效快照内容;从未认证通过时返回 null
|
||||
*/
|
||||
PersonCertSubmitRequest getEffectivePersonCert();
|
||||
|
||||
/**
|
||||
* 提交车辆认证(新增或重新认证)
|
||||
* <p>
|
||||
* 生成一条待审记录,并把该车前一条待审记录置为已作废。
|
||||
* **不修改生效快照**:已认证的车辆在本次审核通过前仍按旧生效值认定,
|
||||
* 因此提交审核不会影响车辆正常接单。
|
||||
*
|
||||
* @param request 车辆认证内容
|
||||
* @return 新生成的认证记录主键
|
||||
*/
|
||||
Long submitVehicleCert(VehicleCertSubmitRequest request);
|
||||
|
||||
/**
|
||||
* 读取某辆车当前生效的认证内容
|
||||
*
|
||||
* @param vehicleId 车辆主键
|
||||
* @return 生效快照内容;从未认证通过时返回 null
|
||||
*/
|
||||
VehicleCertSubmitRequest getEffectiveVehicleCert(Long vehicleId);
|
||||
|
||||
/**
|
||||
* 查询某认证对象的认证记录列表(按提交时间倒序)
|
||||
*
|
||||
* @param bizType 认证对象类型:driver-司机,vehicle-车辆
|
||||
* @param bizId 认证对象主键
|
||||
* @return 认证记录列表
|
||||
*/
|
||||
List<TransportCertRecordVO> listRecords(String bizType, Long bizId);
|
||||
|
||||
/**
|
||||
* 查询单条认证记录详情
|
||||
*
|
||||
* @param recordId 认证记录主键
|
||||
* @return 认证记录;不存在或无权查看时抛业务异常
|
||||
*/
|
||||
TransportCertRecordVO getRecord(Long recordId);
|
||||
|
||||
}
|
||||
+5
@@ -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) {
|
||||
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 司机端认证服务实现
|
||||
* <p>
|
||||
* 提交链路只做三件事:把本次提交的内容**原样**存成一条新的待审记录、
|
||||
* 把该对象前一条待审记录置为已作废、刷新展示缓存。
|
||||
* <p>
|
||||
* ⚠️ 提交**绝不**修改 {@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<TransportCertRecordVO> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一条待审记录
|
||||
* <p>
|
||||
* 状态必须显式设置为待审:框架在插入时会把空的 {@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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按生效快照指针取那条记录
|
||||
* <p>
|
||||
* 只按指针查,不看记录表里任何一条的「最新状态」——这正是生效值与待审值分离的意义。
|
||||
*
|
||||
* @param effectiveRecordId 生效快照指针
|
||||
* @return 生效快照记录;指针为空时返回 null
|
||||
*/
|
||||
private TransportCertRecord findEffectiveRecord(Long effectiveRecordId) {
|
||||
return Func.isEmpty(effectiveRecordId)
|
||||
? null
|
||||
: transportCertRecordService.getById(effectiveRecordId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新司机侧的「最新提交状态」展示缓存
|
||||
* <p>
|
||||
* 只写展示缓存,**不碰生效快照指针**。
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新车辆侧的「最新提交状态」展示缓存
|
||||
* <p>
|
||||
* 只写展示缓存,**不碰生效快照指针**,也不覆盖车辆当前的证件字段。
|
||||
*/
|
||||
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<TransportVehicle> bound = transportVehicleService.list(
|
||||
Wrappers.<TransportVehicle>lambdaQuery()
|
||||
.in(TransportVehicle::getPlateNo,
|
||||
VehiclePlateSupport.splitPlates(driver.getDrivingVehicle())));
|
||||
return bound.stream()
|
||||
.filter(item -> normalized.equals(VehiclePlateSupport.normalize(item.getPlateNo())))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前登录用户能否查看该认证对象的记录
|
||||
* <p>
|
||||
* 司机只能查自己的;车辆必须是本人绑定的。越权一律拒绝,不返回空列表
|
||||
* ——空列表会让调用方以为「没有记录」,而不是「无权查看」。
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
+3
@@ -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<TransportCertR
|
||||
transportCertRecordVO.setUpdateUserName(UserCache.getUserRealName(transportCertRecord.getUpdateUser()));
|
||||
transportCertRecordVO.setSubmitUserName(UserCache.getUserRealName(transportCertRecord.getSubmitUser()));
|
||||
transportCertRecordVO.setAuditUserName(UserCache.getUserRealName(transportCertRecord.getAuditUser()));
|
||||
// 状态文案统一在此翻译,避免小程序、Web、接口各处各造一套说法
|
||||
transportCertRecordVO.setStatusText(CertificationRuleSupport.statusText(transportCertRecord.getStatus()));
|
||||
return transportCertRecordVO;
|
||||
}
|
||||
|
||||
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
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.service.IDriverAppService;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.ITransportCertRecordService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 司机端认证提交链路测试。
|
||||
* <p>
|
||||
* 重点守住票 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<TransportCertRecord> 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<TransportVehicle> 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<TransportCertRecord> 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<TransportCertRecord> 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<TransportCertRecord> 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<Driver> 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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user