From f351bad545d421ed294062201ebb67c08fd73db7 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Fri, 2 Oct 2026 16:46:04 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=8E=A5=E5=8D=95=E5=8F=A0?= =?UTF-8?q?=E5=8A=A0=E8=AE=A4=E8=AF=81=E5=8F=AF=E7=94=A8=E6=80=A7=E9=97=A8?= =?UTF-8?q?=E6=A7=9B=EF=BC=8C=E5=B8=A6=E5=BC=80=E5=85=B3=E5=8F=AF=E5=BF=AB?= =?UTF-8?q?=E9=80=9F=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在原有运营状态(启用/停用)之上叠加一层「是否已认证」: - 车辆:启用 且 存在生效快照;司机:存在生效快照。 判据是**生效快照指针**而非「最新提交状态」展示缓存,这一区别正是本改造的核心: 司机重新提交认证会把展示缓存变成「审核中」,若照它判定,司机一提交就接不了单 ——那恰恰是本次改造要消除的问题。生效快照只在审核通过时切换,故曾通过即持续可用。 两个开关默认开启,可通过配置关闭以便线上快速回退到改造前行为: 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 --- .../impl/DriverWaybillServiceImpl.java | 58 ++++---- .../transport/support/CertificationGate.java | 124 ++++++++++++++++++ .../support/CertificationGateTest.java | 110 ++++++++++++++++ 3 files changed, 270 insertions(+), 22 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertificationGate.java create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertificationGateTest.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index 2181abcf..7cd3b1a4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -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); } + /** + * 接单前的认证门槛校验 + *

+ * 司机的生效快照由其档案持有;车辆按司机绑定的车牌取。 + * 任一不满足即拒绝接单,并给出可读原因。 + * + * @param driver 当前司机 + */ + private void assertCertified(Driver driver) { + if (!certificationGate.isDriverAvailable(driver.getEffectiveCertRecordId())) { + throw new ServiceException(certificationGate.driverUnavailableReason()); + } + if (currentBoundPlates(driver).isEmpty()) { + return; + } + List boundVehicles = transportVehicleService.list( + Wrappers.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 splitPlates(String drivingVehicle) { - String normalized = drivingVehicle.replace(",", ",").replace("、", ",").replace(";", ",") - .replace(";", ",").replace("/", ",").replace("|", ","); - Set 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) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertificationGate.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertificationGate.java new file mode 100644 index 00000000..cd5e4185 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertificationGate.java @@ -0,0 +1,124 @@ +/** + * 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.support; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * 认证可用性门槛 + *

+ * 在原有的运营状态(启用 / 停用)之上**叠加**一层「是否已认证」: + *

+ *

+ * ⚠️ 判据是**生效快照指针**,不是「最新提交状态」展示缓存。 + * 这一区别是本改造的核心:司机重新提交认证会把展示缓存变成「审核中」, + * 若照它判定,司机一提交认证就立刻接不了单——正是要避免的事。 + * 生效快照只在审核通过时才切换,故「曾认证通过即持续可用」。 + *

+ * 两个开关默认开启,可通过配置关闭以便线上快速回退到改造前的行为: + *

+ * transport:
+ *   certification:
+ *     vehicle-gate-enabled: false
+ *     driver-gate-enabled: false
+ * 
+ * + * @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; + + /** + * 判定车辆是否可用 + *

+ * 运营状态优先:停用的车即便已认证也不可用(认证门槛只叠加,不覆盖运营状态)。 + * + * @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 "您尚未通过认证审核,暂不可接单,请先完成认证"; + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertificationGateTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertificationGateTest.java new file mode 100644 index 00000000..08ee25b7 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/CertificationGateTest.java @@ -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; + +/** + * 认证可用性门槛测试。 + *

+ * 这是整个认证改造的**核心验收点**:一辆已认证的车被重新提交认证后,必须**仍可接单**。 + * 改造前的判定只看车辆的启用/停用状态;本门槛叠加一层「是否已认证」。 + * 之所以必须叠加在生效快照上而不是「最新提交状态」,正是因为重新提交会把 + * 「最新提交状态」变成「审核中」——若照它判定,司机一提交认证就立刻接不了单。 + */ +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("认证")); + } + +}