diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java index 9d1c07dc..b7bb4e34 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CertAuditServiceImpl.java @@ -191,7 +191,6 @@ public class CertAuditServiceImpl implements ICertAuditService { private void updateCertTarget(TransportCertRecord record, boolean pass, String rejectReason) { boolean vehicleTarget = TransportCertRecord.BIZ_TYPE_VEHICLE.equals(record.getBizType()); Integer status = pass ? TransportCertRecord.STATUS_APPROVED : TransportCertRecord.STATUS_REJECTED; - String reason = pass ? null : CertificationRuleSupport.reasonForReject(rejectReason); // 只有通过才切指针;驳回时保持 null,框架的 not_null 更新策略会跳过该列 Long newEffectiveId = pass ? record.getId() : null; @@ -199,22 +198,25 @@ public class CertAuditServiceImpl implements ICertAuditService { TransportVehicle vehicle = new TransportVehicle(); vehicle.setId(record.getBizId()); vehicle.setCertificationStatus(status); - vehicle.setCertificationRejectReason(reason); if (pass) { vehicle.setEffectiveCertRecordId(newEffectiveId); // 通过即生效:把这次快照的证件内容一并写回车辆档案,让「车辆档案 = 生效值」成立。 // 不写回的话,审核通过之后管理端与司机端看到的仍是提交之前的旧值—— // 状态显示「已通过」、内容却还是旧的,是最容易让人误判的一种不一致。 - // 与指针合并成同一次 UPDATE,不额外多写一次同一行。 + // 与指针合并成同一次 UPDATE。 applySnapshotToVehicle(vehicle, record.getSnapshotJson()); + } else { + vehicle.setCertificationRejectReason(CertificationRuleSupport.reasonForReject(rejectReason)); } requireTargetUpdated(transportVehicleService.updateById(vehicle), "车辆", record.getBizId()); + if (pass) { + clearVehicleRejectReason(record.getBizId()); + } return; } Driver driver = new Driver(); driver.setId(record.getBizId()); driver.setCertificationStatus(status); - driver.setCertificationRejectReason(reason); // 必须带上 driving_vehicle 原值:该列是 FieldStrategy.ALWAYS, // 更新时忽略 null 直接写,patch 里不带它就会把司机的车辆绑定清成 NULL, // 而车辆列表正是按这一列匹配的(清空即「暂无绑定车辆」)。 @@ -224,10 +226,45 @@ public class CertAuditServiceImpl implements ICertAuditService { // 通过即生效:与车辆侧同理,把这次快照的证件内容一并写回司机档案。 // 不写回的话,司机档案里始终是管理端当初录入的旧值(甚至为空), // 而认证状态已是「已通过」——状态与内容不一致最容易让人误判。 - // 与指针合并成同一次 UPDATE,不额外多写一次同一行。 + // 与指针合并成同一次 UPDATE。 applySnapshotToDriver(driver, record.getSnapshotJson()); + } else { + driver.setCertificationRejectReason(CertificationRuleSupport.reasonForReject(rejectReason)); } requireTargetUpdated(driverService.updateById(driver), "司机", record.getBizId()); + if (pass) { + clearDriverRejectReason(record.getBizId()); + } + } + + /** + * 清空车辆档案上的驳回原因展示缓存 + *

+ * 该列没有驳回原因时必须是 NULL(建表即 DEFAULT NULL)。但框架的 not_null 更新策略 + * 会跳过 patch 里的 null 字段,交给 updateById 的 null 到不了 SQL, + * 「已通过」的车辆会一直挂着上一次的驳回文案,故单独发一条只改该列的语句。 + * 它不碰生效快照指针。 + * + * @param vehicleId 车辆主键 + */ + private void clearVehicleRejectReason(Long vehicleId) { + transportVehicleService.update(Wrappers.lambdaUpdate() + .eq(TransportVehicle::getId, vehicleId) + .set(TransportVehicle::getCertificationRejectReason, null)); + } + + /** + * 清空司机档案上的驳回原因展示缓存 + *

+ * 与车辆侧同理:null 交给 updateById 到不了 SQL,只能单独发一条只改该列的语句。 + * 它不碰生效快照指针,也不碰车辆绑定。 + * + * @param driverId 司机主键 + */ + private void clearDriverRejectReason(Long driverId) { + driverService.update(Wrappers.lambdaUpdate() + .eq(Driver::getId, driverId) + .set(Driver::getCertificationRejectReason, null)); } /** 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 index 7246529d..f172a547 100644 --- 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 @@ -376,6 +376,13 @@ public class DriverCertServiceImpl implements IDriverCertService { patch.setDrivingVehicle(current.getDrivingVehicle()); } driverService.updateById(patch); + if (Func.isBlank(rejectReason)) { + // 重新提交时驳回原因应为空,但框架的 not_null 更新策略会跳过 patch 里的 null, + // 只写 null 到不了 SQL,旧驳回文案会一直挂到审核通过,故单独清一次 + driverService.update(Wrappers.lambdaUpdate() + .eq(Driver::getId, driverId) + .set(Driver::getCertificationRejectReason, null)); + } } /** @@ -389,6 +396,12 @@ public class DriverCertServiceImpl implements IDriverCertService { patch.setCertificationStatus(status); patch.setCertificationRejectReason(rejectReason); transportVehicleService.updateById(patch); + if (Func.isBlank(rejectReason)) { + // 与司机侧同理:null 交给 updateById 到不了 SQL,旧驳回文案会一直挂着 + transportVehicleService.update(Wrappers.lambdaUpdate() + .eq(TransportVehicle::getId, vehicleId) + .set(TransportVehicle::getCertificationRejectReason, null)); + } } /** diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/CertAuditServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/CertAuditServiceImplTest.java index 503311a8..e570361d 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/CertAuditServiceImplTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/CertAuditServiceImplTest.java @@ -1,5 +1,11 @@ package org.springblade.transport.service.impl; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,7 +20,10 @@ import org.springblade.transport.service.IDriverService; import org.springblade.transport.service.ITransportCertRecordService; import org.springblade.transport.service.ITransportVehicleService; +import java.util.Collection; + import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -43,6 +52,16 @@ class CertAuditServiceImplTest { private IDriverService driverService; private CertAuditServiceImpl service; + @BeforeAll + static void initTableInfo() { + // 构造 LambdaUpdateWrapper 需要 MyBatis-Plus 的实体元数据缓存 + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, Driver.class); + TableInfoHelper.initTableInfo(assistant, TransportVehicle.class); + } + @BeforeEach void setUp() { certRecordService = mock(ITransportCertRecordService.class); @@ -330,6 +349,73 @@ class CertAuditServiceImplTest { assertNull(detail.getEffectiveRecordId()); } + // -------------------------------------------- 驳回原因的展示缓存 + + @Test + @DisplayName("通过车辆认证:显式清空档案上的驳回原因(not_null 策略会让 null 到不了 SQL)") + @SuppressWarnings("unchecked") + void approveClearsStaleVehicleRejectReason() { + TransportCertRecord record = vehicleRecord(TransportCertRecord.STATUS_PENDING); + when(certRecordService.getById(RECORD_ID)).thenReturn(record); + when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null)); + + service.audit(RECORD_ID, true, null); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Wrapper.class); + verify(transportVehicleService).update(captor.capture()); + LambdaUpdateWrapper wrapper = + (LambdaUpdateWrapper) captor.getValue(); + assertTrue(wrapper.getSqlSet().contains("certification_reject_reason"), + "通过时必须显式清空驳回原因,实际: " + wrapper.getSqlSet()); + Collection values = wrapper.getParamNameValuePairs().values(); + assertTrue(values.contains(null), "清空必须显式写 null(该列无原因时的取值)"); + assertTrue(wrapper.getSqlSegment().contains("id =") + && values.stream().anyMatch(v -> v instanceof Number + && ((Number) v).longValue() == VEHICLE_ID), + "必须带主键条件且落在被审核的那一行,否则会更新整张表,实际: " + wrapper.getSqlSegment()); + } + + @Test + @DisplayName("通过司机认证:同样显式清空驳回原因,且不顺带动车辆绑定") + @SuppressWarnings("unchecked") + void approveClearsStaleDriverRejectReason() { + TransportCertRecord record = driverRecord(TransportCertRecord.STATUS_PENDING); + when(certRecordService.getById(RECORD_ID)).thenReturn(record); + when(driverService.getById(DRIVER_ID)).thenReturn(driver(null)); + + service.audit(RECORD_ID, true, null); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Wrapper.class); + verify(driverService).update(captor.capture()); + LambdaUpdateWrapper wrapper = (LambdaUpdateWrapper) captor.getValue(); + assertTrue(wrapper.getSqlSet().contains("certification_reject_reason"), + "通过时必须显式清空驳回原因,实际: " + wrapper.getSqlSet()); + Collection values = wrapper.getParamNameValuePairs().values(); + assertTrue(values.contains(null)); + assertTrue(wrapper.getSqlSegment().contains("id =") + && values.stream().anyMatch(v -> v instanceof Number + && ((Number) v).longValue() == DRIVER_ID), + "必须带主键条件且落在被审核的那一行,否则会更新整张表,实际: " + wrapper.getSqlSegment()); + assertFalse(wrapper.getSqlSet().contains("driving_vehicle"), + "清空驳回原因不得顺带动车辆绑定"); + } + + @Test + @DisplayName("驳回:原因去空白后写入档案,且不发清空语句") + void rejectWritesReasonAndDoesNotClear() { + TransportCertRecord record = vehicleRecord(TransportCertRecord.STATUS_PENDING); + when(certRecordService.getById(RECORD_ID)).thenReturn(record); + when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(777L)); + + service.audit(RECORD_ID, false, " 行驶证模糊 "); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TransportVehicle.class); + verify(transportVehicleService).updateById(captor.capture()); + assertEquals("行驶证模糊", captor.getValue().getCertificationRejectReason(), + "驳回原因去首尾空白后落库"); + verify(transportVehicleService, never()).update(any()); + } + // ------------------------------------------------------------ 辅助 private TransportCertRecord vehicleRecord(Integer status) { diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java index 43cac1ed..505f57a1 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java @@ -1,7 +1,13 @@ package org.springblade.transport.service.impl; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.alibaba.fastjson2.JSON; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,6 +24,7 @@ import org.springblade.transport.service.ITransportCertRecordService; import org.springblade.transport.service.ITransportVehicleService; import java.time.LocalDate; +import java.util.Collection; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -53,6 +60,16 @@ class DriverCertServiceImplTest { private ITransportCertRecordService certRecordService; private DriverCertServiceImpl service; + @BeforeAll + static void initTableInfo() { + // 构造 LambdaUpdateWrapper 需要 MyBatis-Plus 的实体元数据缓存 + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, Driver.class); + TableInfoHelper.initTableInfo(assistant, TransportVehicle.class); + } + @BeforeEach void setUp() { driverAppService = mock(IDriverAppService.class); @@ -279,6 +296,32 @@ class DriverCertServiceImplTest { "提交链路不得写生效快照指针"); } + @Test + @DisplayName("个人提交:清空上一次的驳回原因(null 交给 updateById 到不了 SQL)") + @SuppressWarnings("unchecked") + void submitPersonCertClearsStaleRejectReason() { + when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE)); + when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null); + + PersonCertSubmitRequest request = new PersonCertSubmitRequest(); + request.setRealName("张三"); + request.setIdCardNo("110101199001011234"); + + service.submitPersonCert(request); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Wrapper.class); + verify(driverService).update(captor.capture()); + LambdaUpdateWrapper wrapper = (LambdaUpdateWrapper) captor.getValue(); + assertTrue(wrapper.getSqlSet().contains("certification_reject_reason"), + "重新提交时旧驳回原因必须清掉,实际: " + wrapper.getSqlSet()); + Collection values = wrapper.getParamNameValuePairs().values(); + assertTrue(values.contains(null), "清空必须显式写 null"); + assertTrue(wrapper.getSqlSegment().contains("id =") + && values.stream().anyMatch(v -> v instanceof Number + && ((Number) v).longValue() == DRIVER_ID), + "必须带主键条件且落在被审核的那一行,否则会更新整张表,实际: " + wrapper.getSqlSegment()); + } + @Test @DisplayName("个人提交:内容为空必须拒绝") void submitPersonCertRejectsEmpty() { diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java index 14926434..e4639e36 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java @@ -1,6 +1,10 @@ package org.springblade.transport.service.impl; import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -53,6 +57,15 @@ class PersonCertRoundTripTest { private Driver driver; private long idSequence = 100L; + @BeforeAll + static void initTableInfo() { + // 构造 LambdaUpdateWrapper 需要 MyBatis-Plus 的实体元数据缓存 + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, Driver.class); + } + @BeforeEach void setUp() { recordStore.clear();