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
+ * 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;
+
+/**
+ * 认证可用性门槛
+ *
+ * 在原有的运营状态(启用 / 停用)之上**叠加**一层「是否已认证」:
+ *
+ * ⚠️ 判据是**生效快照指针**,不是「最新提交状态」展示缓存。
+ * 这一区别是本改造的核心:司机重新提交认证会把展示缓存变成「审核中」,
+ * 若照它判定,司机一提交认证就立刻接不了单——正是要避免的事。
+ * 生效快照只在审核通过时才切换,故「曾认证通过即持续可用」。
+ *
+ * 两个开关默认开启,可通过配置关闭以便线上快速回退到改造前的行为:
+ *
+ * 运营状态优先:停用的车即便已认证也不可用(认证门槛只叠加,不覆盖运营状态)。
+ *
+ * @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("认证"));
+ }
+
+}
+ *
+ *
+ * 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;
+
+ /**
+ * 判定车辆是否可用
+ *