✨ 接单叠加认证可用性门槛,带开关可快速回退
在原有运营状态(启用/停用)之上叠加一层「是否已认证」: - 车辆:启用 且 存在生效快照;司机:存在生效快照。 判据是**生效快照指针**而非「最新提交状态」展示缓存,这一区别正是本改造的核心: 司机重新提交认证会把展示缓存变成「审核中」,若照它判定,司机一提交就接不了单 ——那恰恰是本次改造要消除的问题。生效快照只在审核通过时切换,故曾通过即持续可用。 两个开关默认开启,可通过配置关闭以便线上快速回退到改造前行为: transport.certification.vehicle-gate-enabled / driver-gate-enabled 门槛落在 DriverWaybillServiceImpl.accept():司机本人与名下**任一**车辆满足即可。 运营状态优先于认证——停用的车即便已认证也不可用(认证只叠加不覆盖)。 顺带消除第三份重复的车牌匹配实现:DriverWaybillServiceImpl 里的 splitPlates/normalizePlate 与 VehiclePlateSupport 逐行相同,改为委托同一实现。 10 个门槛用例(含「重新提交后仍可用」「停用车即便已认证也不可用」) 与全模块 73 个测试通过:mvn -o test -pl blade-service/blade-transport -am
This commit is contained in:
+36
-22
@@ -37,6 +37,10 @@ import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
import org.springblade.transport.pojo.dto.EnrouteSubmitDTO;
|
||||
import org.springblade.transport.pojo.dto.NodeSubmitDTO;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
import org.springblade.transport.support.VehiclePlateSupport;
|
||||
import org.springblade.transport.pojo.entity.ProcessConfig;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.entity.WaybillEnroutePunch;
|
||||
@@ -68,12 +72,10 @@ import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -110,6 +112,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
private final IWaybillService waybillService;
|
||||
private final IDriverService driverService;
|
||||
private final IProcessConfigService processConfigService;
|
||||
private final ITransportVehicleService transportVehicleService;
|
||||
private final CertificationGate certificationGate;
|
||||
private final WaybillEnroutePunchMapper enroutePunchMapper;
|
||||
private final WaybillNodePunchMapper nodePunchMapper;
|
||||
|
||||
@@ -355,6 +359,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean accept(Long id) {
|
||||
Driver driver = requireCurrentDriver();
|
||||
// 认证门槛:司机本人须已认证,且承运车辆须已认证。
|
||||
// 判据是各自持有的生效快照指针,故「已认证的车重新提交认证」不会拦下接单。
|
||||
assertCertified(driver);
|
||||
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
|
||||
waybillService.syncDriverAcceptState(waybill);
|
||||
assertAcceptable(waybill);
|
||||
@@ -368,6 +375,32 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
return waybillService.updateById(waybill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接单前的认证门槛校验
|
||||
* <p>
|
||||
* 司机的生效快照由其档案持有;车辆按司机绑定的车牌取。
|
||||
* 任一不满足即拒绝接单,并给出可读原因。
|
||||
*
|
||||
* @param driver 当前司机
|
||||
*/
|
||||
private void assertCertified(Driver driver) {
|
||||
if (!certificationGate.isDriverAvailable(driver.getEffectiveCertRecordId())) {
|
||||
throw new ServiceException(certificationGate.driverUnavailableReason());
|
||||
}
|
||||
if (currentBoundPlates(driver).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<TransportVehicle> boundVehicles = transportVehicleService.list(
|
||||
Wrappers.<TransportVehicle>lambdaQuery()
|
||||
.in(TransportVehicle::getPlateNo, currentBoundPlates(driver)));
|
||||
boolean anyCertified = boundVehicles.stream().anyMatch(vehicle ->
|
||||
certificationGate.isVehicleAvailable(
|
||||
Integer.valueOf(1).equals(vehicle.getStatus()), vehicle.getEffectiveCertRecordId()));
|
||||
if (!anyCertified) {
|
||||
throw new ServiceException(certificationGate.vehicleUnavailableReason());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean reject(Long id, String reason) {
|
||||
@@ -563,26 +596,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
private List<String> splitPlates(String drivingVehicle) {
|
||||
String normalized = drivingVehicle.replace(",", ",").replace("、", ",").replace(";", ",")
|
||||
.replace(";", ",").replace("/", ",").replace("|", ",");
|
||||
Set<String> plates = new LinkedHashSet<>();
|
||||
for (String part : Func.toStrList(",", normalized)) {
|
||||
if (Func.isEmpty(part)) {
|
||||
continue;
|
||||
}
|
||||
String plate = normalizePlate(part);
|
||||
if (Func.isNotEmpty(plate)) {
|
||||
plates.add(plate);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(plates);
|
||||
}
|
||||
|
||||
private String normalizePlate(String plateNo) {
|
||||
if (Func.isEmpty(plateNo)) {
|
||||
return "";
|
||||
}
|
||||
return plateNo.trim().replaceAll("[\\s\\-·•..]", "").toUpperCase(Locale.ROOT);
|
||||
return VehiclePlateSupport.splitPlates(drivingVehicle);
|
||||
}
|
||||
|
||||
private int normalizePreviewSize(Integer size) {
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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.support;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 认证可用性门槛
|
||||
* <p>
|
||||
* 在原有的运营状态(启用 / 停用)之上**叠加**一层「是否已认证」:
|
||||
* <ul>
|
||||
* <li>车辆:运营状态为启用,且存在生效快照;</li>
|
||||
* <li>司机:存在生效快照。</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* ⚠️ 判据是**生效快照指针**,不是「最新提交状态」展示缓存。
|
||||
* 这一区别是本改造的核心:司机重新提交认证会把展示缓存变成「审核中」,
|
||||
* 若照它判定,司机一提交认证就立刻接不了单——正是要避免的事。
|
||||
* 生效快照只在审核通过时才切换,故「曾认证通过即持续可用」。
|
||||
* <p>
|
||||
* 两个开关默认开启,可通过配置关闭以便线上快速回退到改造前的行为:
|
||||
* <pre>
|
||||
* transport:
|
||||
* certification:
|
||||
* vehicle-gate-enabled: false
|
||||
* driver-gate-enabled: false
|
||||
* </pre>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Component
|
||||
@Getter
|
||||
@Setter
|
||||
public class CertificationGate {
|
||||
|
||||
/**
|
||||
* 车辆认证门槛开关
|
||||
*/
|
||||
@Value("${transport.certification.vehicle-gate-enabled:true}")
|
||||
private boolean vehicleGateEnabled = true;
|
||||
|
||||
/**
|
||||
* 司机认证门槛开关
|
||||
*/
|
||||
@Value("${transport.certification.driver-gate-enabled:true}")
|
||||
private boolean driverGateEnabled = true;
|
||||
|
||||
/**
|
||||
* 判定车辆是否可用
|
||||
* <p>
|
||||
* 运营状态优先:停用的车即便已认证也不可用(认证门槛只叠加,不覆盖运营状态)。
|
||||
*
|
||||
* @param enabledState 运营状态:TRUE 启用
|
||||
* @param effectiveRecordId 生效快照指针
|
||||
* @return 是否可用
|
||||
*/
|
||||
public boolean isVehicleAvailable(Boolean enabledState, Long effectiveRecordId) {
|
||||
if (!Boolean.TRUE.equals(enabledState)) {
|
||||
return false;
|
||||
}
|
||||
if (!vehicleGateEnabled) {
|
||||
return true;
|
||||
}
|
||||
return CertificationRuleSupport.isCertified(effectiveRecordId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定司机是否可接单
|
||||
*
|
||||
* @param effectiveRecordId 生效快照指针
|
||||
* @return 是否可用
|
||||
*/
|
||||
public boolean isDriverAvailable(Long effectiveRecordId) {
|
||||
if (!driverGateEnabled) {
|
||||
return true;
|
||||
}
|
||||
return CertificationRuleSupport.isCertified(effectiveRecordId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 车辆不可用的提示文案
|
||||
*
|
||||
* @return 面向司机的中文提示
|
||||
*/
|
||||
public String vehicleUnavailableReason() {
|
||||
return "该车辆尚未通过认证审核,暂不可接单,请先提交认证";
|
||||
}
|
||||
|
||||
/**
|
||||
* 司机不可用的提示文案
|
||||
*
|
||||
* @return 面向司机的中文提示
|
||||
*/
|
||||
public String driverUnavailableReason() {
|
||||
return "您尚未通过认证审核,暂不可接单,请先完成认证";
|
||||
}
|
||||
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 认证可用性门槛测试。
|
||||
* <p>
|
||||
* 这是整个认证改造的**核心验收点**:一辆已认证的车被重新提交认证后,必须**仍可接单**。
|
||||
* 改造前的判定只看车辆的启用/停用状态;本门槛叠加一层「是否已认证」。
|
||||
* 之所以必须叠加在生效快照上而不是「最新提交状态」,正是因为重新提交会把
|
||||
* 「最新提交状态」变成「审核中」——若照它判定,司机一提交认证就立刻接不了单。
|
||||
*/
|
||||
class CertificationGateTest {
|
||||
|
||||
private CertificationGate gate;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gate = new CertificationGate();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 开关
|
||||
|
||||
@Test
|
||||
@DisplayName("门槛默认开启")
|
||||
void enabledByDefault() {
|
||||
assertTrue(gate.isVehicleGateEnabled());
|
||||
assertTrue(gate.isDriverGateEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("关闭后行为回到改造前:不因未认证而拦截(便于快速回退)")
|
||||
void disabledLetsEveryoneThrough() {
|
||||
gate.setVehicleGateEnabled(false);
|
||||
gate.setDriverGateEnabled(false);
|
||||
|
||||
assertTrue(gate.isVehicleAvailable(Boolean.TRUE, null));
|
||||
assertTrue(gate.isDriverAvailable(null));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 车辆
|
||||
|
||||
@Test
|
||||
@DisplayName("车辆:有生效快照即可用(核心:重新提交不影响可用性)")
|
||||
void vehicleWithEffectiveSnapshotIsAvailable() {
|
||||
assertTrue(gate.isVehicleAvailable(Boolean.TRUE, 7001L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("车辆:重新提交后【状态变成审核中】但仍有生效快照 → 仍可用")
|
||||
void resubmittedVehicleStaysAvailable() {
|
||||
// 关键场景:司机提交了新资料,展示缓存变成「审核中」,
|
||||
// 但 effective_cert_record_id 仍指着上一次通过的记录。
|
||||
// 门槛只看指针,故这辆车照跑不误——这就是本次改造要保住的东西。
|
||||
assertTrue(gate.isVehicleAvailable(Boolean.TRUE, 7001L),
|
||||
"已认证的车重新提交后必须仍可接单,否则司机一提交就停工");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("车辆:从未认证(无生效快照)被拒绝")
|
||||
void vehicleWithoutEffectiveSnapshotIsRejected() {
|
||||
assertFalse(gate.isVehicleAvailable(Boolean.TRUE, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("车辆:停用状态优先于认证——停用的车即便有生效快照也不可用")
|
||||
void disabledVehicleIsRejectedEvenIfCertified() {
|
||||
assertFalse(gate.isVehicleAvailable(Boolean.FALSE, 7001L),
|
||||
"启用/停用是运营状态,认证门槛只叠加不覆盖");
|
||||
assertFalse(gate.isVehicleAvailable(null, 7001L));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 司机
|
||||
|
||||
@Test
|
||||
@DisplayName("司机:有生效快照即可用")
|
||||
void driverWithEffectiveSnapshotIsAvailable() {
|
||||
assertTrue(gate.isDriverAvailable(9001L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("司机:从未认证被拒绝")
|
||||
void driverWithoutEffectiveSnapshotIsRejected() {
|
||||
assertFalse(gate.isDriverAvailable(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("司机门槛可单独关闭(改造期间用于渐进放开)")
|
||||
void driverGateCanBeDisabledIndependently() {
|
||||
gate.setDriverGateEnabled(false);
|
||||
assertTrue(gate.isDriverAvailable(null), "司机门槛关闭时不应拦截");
|
||||
assertFalse(gate.isVehicleAvailable(Boolean.TRUE, null), "车辆门槛不受影响");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 提示文案
|
||||
|
||||
@Test
|
||||
@DisplayName("拒绝原因可读,且不泄露内部字段名")
|
||||
void rejectReasonIsReadable() {
|
||||
assertTrue(gate.vehicleUnavailableReason().contains("认证"));
|
||||
assertFalse(gate.vehicleUnavailableReason().contains("effective_cert_record_id"));
|
||||
assertTrue(gate.driverUnavailableReason().contains("认证"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user