From f3dc451802a473a5361d31647da2e7641879ed23 Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Wed, 7 Oct 2026 19:15:29 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E4=B8=AA=E4=BA=BA=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E5=90=8E=E5=A7=93=E5=90=8D=E4=B8=8E=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E8=AF=81=E5=8F=B7=E4=B9=9F=E5=86=99=E5=9B=9E=E5=8F=B8?= =?UTF-8?q?=E6=9C=BA=E6=A1=A3=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审核通过时只写回证件内容,姓名与身份证号被当作「身份标识」跳过:司机申报「黄大富」,档案里却还是建档时的「黄宇富」,司机端卡片显示档案名、「重新认证」表单回填快照名,同一个页面上两个名字。现在审核通过即认可整份申报内容,姓名与身份证号一并写回,档案与生效值一致。身份证号写回前先确认它没有落在另一名司机名下——该列没有唯一索引,直接写会静默产生两条同证件号的档案,管理端的证件号查重从此失效。 --- .../service/impl/CertAuditServiceImpl.java | 50 ++++++++++++++++--- .../service/impl/PersonCertRoundTripTest.java | 36 +++++++++++-- 2 files changed, 75 insertions(+), 11 deletions(-) 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 4fe25a83..9d1c07dc 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 @@ -27,6 +27,7 @@ package org.springblade.transport.service.impl; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.secure.utils.AuthUtil; @@ -51,6 +52,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; /** * 认证审核服务实现 @@ -268,19 +270,21 @@ public class CertAuditServiceImpl implements ICertAuditService { /** * 把生效快照里的司机证件字段搬进司机档案(就地写入同一个 patch 对象) *

- * 只搬**司机自己申报的证件内容**,不碰两类字段: - *

+ * 搬**司机自己申报的全部实名内容**,姓名与身份证号也在其中:审核通过即认可这次申报, + * 档案必须与生效值一致;否则司机端卡片显示档案里的名字、「重新认证」表单回填快照里的名字, + * 同一个页面上出现两个名字。 + *

+ * 仍不碰 {@code driverType} / {@code posts} / {@code drivingVehicle} / {@code organizationName}: + * 内部管理归属与派工关系由管理端维护,不该被司机的提交内容覆盖。 + *

* 快照里为 null 的字段不会写(框架的 not_null 更新策略会跳过), * 因此不会把管理端填过、司机端没填的字段清空。 *

* 快照缺失、无法解析或不是当前字段定义时**只跳过写回,不阻断审核**: * 审核本身是独立于写回的业务动作(切指针、置状态),不该因为一份读不懂的快照而失败; * 何况历史快照本来就存在,阻断会让这些对象再也审不过去。 + * 申报的身份证号已落在另一名司机名下时是例外——那会把两条档案写成同一个证件号, + * 必须阻断。 * * @param driver 待更新的司机 patch * @param snapshotJson 该条记录的全量提交内容 @@ -304,6 +308,11 @@ public class CertAuditServiceImpl implements ICertAuditService { if (Func.isEmpty(snapshot.getIdCardNo()) && Func.isEmpty(snapshot.getDriverFileNo())) { return; } + // 身份证号要落到档案上,先确认它没有落在别人名下:该列没有唯一索引, + // 直接写会静默产生两条同证件号的档案,管理端的证件号查重从此失效 + requireIdCardNotTaken(snapshot.getIdCardNo(), driver.getId()); + driver.setDriverName(snapshot.getRealName()); + driver.setIdCardNo(snapshot.getIdCardNo()); driver.setIdCardFront(snapshot.getIdCardFrontUrl()); driver.setIdCardBack(snapshot.getIdCardBackUrl()); driver.setIdCardValidStart(snapshot.getIdCardValidStart()); @@ -325,6 +334,33 @@ public class CertAuditServiceImpl implements ICertAuditService { driver.setQualificationLongTerm(snapshot.getQualificationLongTerm()); } + /** + * 身份证号不得落在另一名司机名下 + *

+ * 司机申报的实名内容会写回档案,其中身份证号是管理端建档时的查重判据, + * 而 {@code blade_transport_driver.id_card_no} 上没有唯一索引:若申报的证件号 + * 已属于另一条档案,直接写会静默产生两条同证件号的档案,后续建档查重、 + * 按证件号识别同一人都将失效。此处阻断审核并整体回滚,交由管理端先处理档案归属。 + *

+ * 证件号与当前档案相同(绝大多数情况)、或库里尚无该号时不阻断; + * 快照没带证件号时无从判断,同样不阻断(历史快照就是这种)。 + * + * @param idCardNo 快照里的身份证号 + * @param driverId 本次要写入的司机主键 + */ + private void requireIdCardNotTaken(String idCardNo, Long driverId) { + if (Func.isEmpty(idCardNo)) { + return; + } + Driver owner = driverService.getOne(Wrappers.lambdaQuery() + .eq(Driver::getIdCardNo, idCardNo) + .last("LIMIT 1")); + if (owner != null && !Objects.equals(owner.getId(), driverId)) { + throw new ServiceException("身份证号 " + idCardNo + " 已属于另一名司机档案(ID:" + owner.getId() + + "),本次审核已回滚、未生效;请先处理档案归属"); + } + } + /** * 取从业资格证封面页地址,兼容改名前的旧键 *

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 ca07ae88..14926434 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 @@ -4,6 +4,7 @@ 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.springblade.core.log.exception.ServiceException; import org.springblade.transport.pojo.dto.PersonCertSubmitRequest; import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.mapper.TransportCertRecordMapper; @@ -18,6 +19,8 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; 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; @@ -217,6 +220,12 @@ class PersonCertRoundTripTest { * 这类只有真实更新策略才暴露的问题。 */ private void copyCertFields(Driver patch, Driver target) { + if (patch.getDriverName() != null) { + target.setDriverName(patch.getDriverName()); + } + if (patch.getIdCardNo() != null) { + target.setIdCardNo(patch.getIdCardNo()); + } if (patch.getIdCardFront() != null) { target.setIdCardFront(patch.getIdCardFront()); } @@ -292,19 +301,38 @@ class PersonCertRoundTripTest { } @Test - @DisplayName("审核通过:不覆盖姓名与身份证号(身份标识由管理端建档确定)") - void approveDoesNotOverwriteIdentity() { + @DisplayName("审核通过:姓名与身份证号跟着认证资料走,档案与生效值不再各说各话") + void approveOverwritesIdentity() { driver.setDriverName("张三(档案名)"); driver.setIdCardNo("110101199001011234"); Long recordId = driverCertService.submitPersonCert(personCert("张三改名", "110101199001011234")); certAuditService.audit(recordId, true, null); - assertEquals("张三(档案名)", driver.getDriverName(), - "姓名是建档时确定的身份字段,司机端提交不得把它改掉"); + // 审核通过即认可这次申报:档案里必须也是申报的姓名, + // 否则司机端卡片显示档案名、「重新认证」表单回填快照名,同一个页面上两个名字 + assertEquals("张三改名", driver.getDriverName()); assertEquals("110101199001011234", driver.getIdCardNo()); } + @Test + @DisplayName("审核通过:申报的身份证号已属于另一名司机时阻断审核,不得产生两条同号档案") + void approveRejectsIdCardTakenByAnotherDriver() { + Driver other = new Driver(); + other.setId(8888L); + other.setIdCardNo("110101199001011999"); + when(driverService.getOne(any())).thenReturn(other); + + Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011999")); + + ServiceException exception = assertThrows(ServiceException.class, + () -> certAuditService.audit(recordId, true, null)); + assertTrue(exception.getMessage().contains("另一名司机档案"), exception.getMessage()); + // 真实环境下这次审核由 @Transactional 整体回滚(记录仍是待审); + // 单测没有事务,故这里校验更直接的一点:拦截发生在写档案之前,别人的证件号没被写进来 + assertNull(driver.getIdCardNo(), "不得把属于别人的证件号写进当前档案"); + } + @Test @DisplayName("驳回:证件内容一个字都不写回档案") void rejectWritesNothingBack() {