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
new file mode 100644
index 00000000..2015fc67
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/PersonCertRoundTripTest.java
@@ -0,0 +1,217 @@
+package org.springblade.transport.service.impl;
+
+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.transport.pojo.dto.PersonCertSubmitRequest;
+import org.springblade.transport.pojo.entity.Driver;
+import org.springblade.transport.pojo.entity.TransportCertRecord;
+import org.springblade.transport.service.IDriverAppService;
+import org.springblade.transport.service.IDriverService;
+import org.springblade.transport.service.ITransportCertRecordService;
+import org.springblade.transport.service.ITransportVehicleService;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * 个人认证的「提交 → 生效值不变 → 审核通过 → 生效值更新」闭环测试。
+ *
+ * 这是司机认证最关键的一条业务闭环,也是单看提交侧或审核侧都验证不到的:
+ * 提交之后司机侧查到的必须仍是**旧生效值**(否则等于提交即自动认证),
+ * 审核通过之后才变成**新值**(否则认证永远不生效)。
+ *
+ * 用一个内存版的记录存储把两侧接起来,避免两边各自 mock 成「恰好一致」。
+ */
+class PersonCertRoundTripTest {
+
+ private static final Long DRIVER_ID = 9001L;
+
+ private IDriverAppService driverAppService;
+ private IDriverService driverService;
+ private ITransportCertRecordService certRecordService;
+ private DriverCertServiceImpl driverCertService;
+ private CertAuditServiceImpl certAuditService;
+
+ /**
+ * 内存记录表:Id → 记录
+ */
+ private final Map recordStore = new HashMap<>();
+ private Driver driver;
+ private long idSequence = 100L;
+
+ @BeforeEach
+ void setUp() {
+ recordStore.clear();
+ idSequence = 100L;
+
+ driverAppService = mock(IDriverAppService.class);
+ driverService = mock(IDriverService.class);
+ certRecordService = mock(ITransportCertRecordService.class);
+ ITransportVehicleService transportVehicleService = mock(ITransportVehicleService.class);
+
+ driver = new Driver();
+ driver.setId(DRIVER_ID);
+ driver.setDriverName("张三");
+ when(driverAppService.currentDriverEntity()).thenAnswer(invocation -> driver);
+ // 司机表只持有指针,指针是被审核链路改的,这里让桩跟着内存状态走
+ when(driverService.updateById(any(Driver.class))).thenAnswer(invocation -> {
+ Driver patch = invocation.getArgument(0);
+ if (patch.getEffectiveCertRecordId() != null) {
+ driver.setEffectiveCertRecordId(patch.getEffectiveCertRecordId());
+ }
+ return true;
+ });
+
+ stubRecordStore();
+
+ driverCertService = new DriverCertServiceImpl(driverAppService, driverService,
+ transportVehicleService, certRecordService);
+ certAuditService = new CertAuditServiceImpl(certRecordService, transportVehicleService, driverService);
+ }
+
+ @Test
+ @DisplayName("提交后仍是旧生效值,审核通过后才变新值")
+ void submitThenApprove() {
+ // 1) 先走一遍完整流程,让该司机有一个生效值
+ Long firstRecordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
+ certAuditService.audit(firstRecordId, true, null);
+ assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
+ "审核通过后应能查到生效值");
+
+ // 2) 提交新内容:生效值必须**不变**
+ Long secondRecordId = driverCertService.submitPersonCert(personCert("张三改名", "110101199001011234"));
+ assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
+ "提交只产生待审记录,不得改变生效值——否则等于提交即自动认证");
+ assertEquals(firstRecordId, driver.getEffectiveCertRecordId(),
+ "指针应仍指向原来那条已通过记录");
+
+ // 3) 审核通过:生效值才变成新值
+ certAuditService.audit(secondRecordId, true, null);
+ assertEquals("张三改名", driverCertService.getEffectivePersonCert().getRealName(),
+ "审核通过后生效值应切到新记录");
+ assertEquals(secondRecordId, driver.getEffectiveCertRecordId());
+ }
+
+ @Test
+ @DisplayName("驳回后生效值不变(已认证对象不会被驳回下线)")
+ void rejectKeepsEffectiveValue() {
+ Long firstRecordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
+ certAuditService.audit(firstRecordId, true, null);
+
+ Long secondRecordId = driverCertService.submitPersonCert(personCert("张三改名", "110101199001011234"));
+ certAuditService.audit(secondRecordId, false, "证件照片模糊");
+
+ assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
+ "驳回只终结该次提交,既得认证必须保持有效");
+ assertEquals(firstRecordId, driver.getEffectiveCertRecordId());
+ }
+
+ @Test
+ @DisplayName("从未认证过时,审核通过前查不到生效值")
+ void noEffectiveBeforeFirstApproval() {
+ assertNull(driverCertService.getEffectivePersonCert());
+
+ Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
+ assertNull(driverCertService.getEffectivePersonCert(),
+ "待审期间不应有生效值");
+
+ certAuditService.audit(recordId, true, null);
+ assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName());
+ }
+
+ @Test
+ @DisplayName("连续提交两次,不存在两条待审,且旧待审被作废")
+ void consecutiveSubmitsLeaveSinglePending() {
+ Long first = driverCertService.submitPersonCert(personCert("第一次", "110101199001011234"));
+ Long second = driverCertService.submitPersonCert(personCert("第二次", "110101199001011234"));
+
+ assertEquals(TransportCertRecord.STATUS_SUPERSEDED, recordStore.get(first).getStatus(),
+ "前一条待审应被作废");
+ assertEquals(TransportCertRecord.STATUS_PENDING, recordStore.get(second).getStatus());
+
+ long pendingCount = recordStore.values().stream()
+ .filter(record -> TransportCertRecord.STATUS_PENDING.equals(record.getStatus()))
+ .count();
+ assertEquals(1L, pendingCount, "同一司机至多一条待审");
+
+ // 被作废的那条不能再被审核(否则会把已作废的记录变成生效值)
+ org.junit.jupiter.api.Assertions.assertThrows(
+ org.springblade.core.log.exception.ServiceException.class,
+ () -> certAuditService.audit(first, true, null));
+ }
+
+ @Test
+ @DisplayName("已通过的记录不可再被审核(防止把生效快照改坏)")
+ void approvedRecordCannotBeReaudited() {
+ Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
+ certAuditService.audit(recordId, true, null);
+
+ org.junit.jupiter.api.Assertions.assertThrows(
+ org.springblade.core.log.exception.ServiceException.class,
+ () -> certAuditService.audit(recordId, false, "事后反悔"));
+ assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
+ "重复审核被拒后,生效值不受影响");
+ }
+
+ // ------------------------------------------------------------ 内存记录存储
+
+ /**
+ * 用内存 Map 承接记录读写,让两个服务真实地共享同一份状态
+ */
+ private void stubRecordStore() {
+ when(certRecordService.save(any(TransportCertRecord.class))).thenAnswer(invocation -> {
+ TransportCertRecord record = invocation.getArgument(0);
+ record.setId(++idSequence);
+ recordStore.put(record.getId(), copyOf(record));
+ return true;
+ });
+ when(certRecordService.updateById(any(TransportCertRecord.class))).thenAnswer(invocation -> {
+ TransportCertRecord patch = invocation.getArgument(0);
+ TransportCertRecord stored = recordStore.get(patch.getId());
+ if (stored != null) {
+ if (patch.getStatus() != null) {
+ stored.setStatus(patch.getStatus());
+ }
+ stored.setRejectReason(patch.getRejectReason());
+ stored.setAuditTime(patch.getAuditTime());
+ stored.setAuditUser(patch.getAuditUser());
+ }
+ return true;
+ });
+ when(certRecordService.getById(anyLong()))
+ .thenAnswer(invocation -> recordStore.get(invocation.getArgument(0, Long.class)));
+ when(certRecordService.selectPendingRecord(any(), anyLong())).thenAnswer(invocation -> {
+ String bizType = invocation.getArgument(0);
+ Long bizId = invocation.getArgument(1);
+ return recordStore.values().stream()
+ .filter(record -> bizType.equals(record.getBizType())
+ && bizId.equals(record.getBizId())
+ && TransportCertRecord.STATUS_PENDING.equals(record.getStatus()))
+ .findFirst()
+ .orElse(null);
+ });
+ }
+
+ private TransportCertRecord copyOf(TransportCertRecord source) {
+ return JSON.parseObject(JSON.toJSONString(source), TransportCertRecord.class);
+ }
+
+ private PersonCertSubmitRequest personCert(String realName, String idCardNo) {
+ PersonCertSubmitRequest request = new PersonCertSubmitRequest();
+ request.setRealName(realName);
+ request.setIdCardNo(idCardNo);
+ request.setIdCardFrontUrl("http://example.com/front.jpg");
+ request.setIdCardBackUrl("http://example.com/back.jpg");
+ return request;
+ }
+
+}