diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PersonCertSubmitRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PersonCertSubmitRequest.java index 29f20d5c..c33d030f 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PersonCertSubmitRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PersonCertSubmitRequest.java @@ -77,12 +77,20 @@ public class PersonCertSubmitRequest implements Serializable { @Schema(description = "驾驶证长期有效") private Integer driverLongTerm; + @Schema(description = "从业资格证类型") + private String qualificationType; @Schema(description = "从业资格证号") private String qualificationNo; - @Schema(description = "从业资格证照片") - private String qualificationUrl; + @Schema(description = "从业资格证封面页") + private String qualificationFront; + @Schema(description = "从业资格证内容页") + private String qualificationBack; + @Schema(description = "从业资格证有效期起") + private LocalDate qualificationValidStart; @Schema(description = "从业资格证有效期止") private LocalDate qualificationValidEnd; + @Schema(description = "从业资格证长期有效") + private Integer qualificationLongTerm; @Schema(description = "大头照") private String headPhoto; 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 5da2ee5a..89d0b0e6 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 @@ -33,6 +33,7 @@ import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.UserCache; import org.springblade.transport.mapper.TransportCertRecordMapper; +import org.springblade.transport.pojo.dto.PersonCertSubmitRequest; import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest; import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.pojo.entity.TransportCertRecord; @@ -214,10 +215,98 @@ public class CertAuditServiceImpl implements ICertAuditService { driver.setCertificationRejectReason(reason); if (pass) { driver.setEffectiveCertRecordId(newEffectiveId); + // 通过即生效:与车辆侧同理,把这次快照的证件内容一并写回司机档案。 + // 不写回的话,司机档案里始终是管理端当初录入的旧值(甚至为空), + // 而认证状态已是「已通过」——状态与内容不一致最容易让人误判。 + // 与指针合并成同一次 UPDATE,不额外多写一次同一行。 + applySnapshotToDriver(driver, record.getSnapshotJson()); } driverService.updateById(driver); } + /** + * 把生效快照里的司机证件字段搬进司机档案(就地写入同一个 patch 对象) + *
+ * 只搬**司机自己申报的证件内容**,不碰两类字段: + *
+ * 快照缺失、无法解析或不是当前字段定义时**只跳过写回,不阻断审核**: + * 审核本身是独立于写回的业务动作(切指针、置状态),不该因为一份读不懂的快照而失败; + * 何况历史快照本来就存在,阻断会让这些对象再也审不过去。 + * + * @param driver 待更新的司机 patch + * @param snapshotJson 该条记录的全量提交内容 + */ + private void applySnapshotToDriver(Driver driver, String snapshotJson) { + if (Func.isEmpty(snapshotJson)) { + return; + } + PersonCertSubmitRequest snapshot; + try { + snapshot = JSON.parseObject(snapshotJson, PersonCertSubmitRequest.class); + } catch (Exception exception) { + return; + } + if (snapshot == null) { + return; + } + // 字段名对不上时 JSON 会解析成「全是 null」而不报错(历史快照就是这种情况), + // 那样写回等于什么也没写,还会把档案里已有的值当成「没填」而跳过。 + // 故用证件内容里的关键字段判定「这份快照是否是当前格式」,不是就不写。 + if (Func.isEmpty(snapshot.getIdCardNo()) && Func.isEmpty(snapshot.getDriverFileNo())) { + return; + } + driver.setIdCardFront(snapshot.getIdCardFrontUrl()); + driver.setIdCardBack(snapshot.getIdCardBackUrl()); + driver.setIdCardValidStart(snapshot.getIdCardValidStart()); + driver.setIdCardValidEnd(snapshot.getIdCardValidEnd()); + driver.setIdCardLongTerm(snapshot.getIdCardLongTerm()); + driver.setHeadPhoto(snapshot.getHeadPhoto()); + driver.setDrivingType(snapshot.getDriverClass()); + driver.setDrivingLicenseNo(snapshot.getDriverFileNo()); + driver.setDrivingLicenseStartDate(snapshot.getDriverValidStart()); + driver.setDrivingLicenseEndDate(snapshot.getDriverValidEnd()); + driver.setDrivingLicenseLongTerm(snapshot.getDriverLongTerm()); + driver.setDrivingLicenseFront(snapshot.getDriverLicenseFrontUrl()); + driver.setDrivingLicenseBack(snapshot.getDriverLicenseBackUrl()); + driver.setQualificationType(snapshot.getQualificationType()); + driver.setQualificationNo(snapshot.getQualificationNo()); + driver.setQualificationFront(qualificationFrontOf(snapshot, snapshotJson)); + driver.setQualificationBack(snapshot.getQualificationBack()); + driver.setQualificationEndDate(snapshot.getQualificationValidEnd()); + driver.setQualificationLongTerm(snapshot.getQualificationLongTerm()); + } + + /** + * 取从业资格证封面页地址,兼容改名前的旧键 + *
+ * 该字段原叫 {@code qualificationUrl},与司机档案列 {@code qualification_front} 不一致, + * 已统一为 {@code qualificationFront}。改名之前提交的记录里存的仍是旧键, + * 直接按新键取值会得到 null,审核通过后反而把已填的封面页丢掉,故这里回退读一次旧键。 + * + * @param snapshot 已解析的快照(可能因旧键而为 null) + * @param snapshotJson 原始快照 JSON,用于回退读取旧键 + * @return 封面页地址;新旧键都没有时为 null + */ + private String qualificationFrontOf(PersonCertSubmitRequest snapshot, String snapshotJson) { + if (Func.isNotEmpty(snapshot.getQualificationFront())) { + return snapshot.getQualificationFront(); + } + try { + return JSON.parseObject(snapshotJson).getString("qualificationUrl"); + } catch (Exception exception) { + // 旧键缺失或 JSON 结构异常都不该阻断写回,没有封面页就不写这一列 + return null; + } + } + /** * 把生效快照里的车辆证件字段搬进车辆档案(就地写入同一个 patch 对象) *
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 deb2de2b..b72b4182 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 @@ -65,12 +65,14 @@ class PersonCertRoundTripTest { 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()); } + copyCertFields(patch, driver); return true; }); @@ -208,6 +210,48 @@ class PersonCertRoundTripTest { return JSON.parseObject(JSON.toJSONString(source), TransportCertRecord.class); } + /** + * 模拟框架的 not_null 更新策略:patch 里为 null 的列不写,非 null 才覆盖 + *
+ * 单测里直接整对象覆盖会丢掉这个语义,从而验不出「管理端填过、司机端没填的字段被清空」 + * 这类只有真实更新策略才暴露的问题。 + */ + private void copyCertFields(Driver patch, Driver target) { + if (patch.getIdCardFront() != null) { + target.setIdCardFront(patch.getIdCardFront()); + } + if (patch.getIdCardBack() != null) { + target.setIdCardBack(patch.getIdCardBack()); + } + if (patch.getHeadPhoto() != null) { + target.setHeadPhoto(patch.getHeadPhoto()); + } + if (patch.getDrivingType() != null) { + target.setDrivingType(patch.getDrivingType()); + } + if (patch.getDrivingLicenseNo() != null) { + target.setDrivingLicenseNo(patch.getDrivingLicenseNo()); + } + if (patch.getDrivingLicenseFront() != null) { + target.setDrivingLicenseFront(patch.getDrivingLicenseFront()); + } + if (patch.getDrivingLicenseBack() != null) { + target.setDrivingLicenseBack(patch.getDrivingLicenseBack()); + } + if (patch.getQualificationType() != null) { + target.setQualificationType(patch.getQualificationType()); + } + if (patch.getQualificationNo() != null) { + target.setQualificationNo(patch.getQualificationNo()); + } + if (patch.getQualificationFront() != null) { + target.setQualificationFront(patch.getQualificationFront()); + } + if (patch.getQualificationBack() != null) { + target.setQualificationBack(patch.getQualificationBack()); + } + } + private PersonCertSubmitRequest personCert(String realName, String idCardNo) { PersonCertSubmitRequest request = new PersonCertSubmitRequest(); request.setRealName(realName); @@ -217,4 +261,77 @@ class PersonCertRoundTripTest { return request; } + @Test + @DisplayName("审核通过:证件内容写回司机档案,档案与生效值一致") + void approveWritesCertContentBackToDriver() { + PersonCertSubmitRequest request = personCert("张三", "110101199001011234"); + request.setHeadPhoto("http://example.com/head.jpg"); + request.setDriverClass("A2"); + request.setDriverFileNo("450100769875"); + request.setDriverLicenseFrontUrl("http://example.com/dl-front.jpg"); + request.setDriverLicenseBackUrl("http://example.com/dl-back.jpg"); + request.setQualificationType("道路旅客运输"); + request.setQualificationNo("Q0001"); + request.setQualificationFront("http://example.com/q-front.jpg"); + request.setQualificationBack("http://example.com/q-back.jpg"); + + Long recordId = driverCertService.submitPersonCert(request); + certAuditService.audit(recordId, true, null); + + // 档案里必须是这次提交的内容:只切指针、不写内容的话, + // 认证状态显示「已通过」而档案仍是旧值,管理端与司机端会各看各的 + assertEquals("http://example.com/head.jpg", driver.getHeadPhoto()); + assertEquals("A2", driver.getDrivingType()); + assertEquals("450100769875", driver.getDrivingLicenseNo()); + assertEquals("http://example.com/dl-front.jpg", driver.getDrivingLicenseFront()); + assertEquals("http://example.com/dl-back.jpg", driver.getDrivingLicenseBack()); + assertEquals("道路旅客运输", driver.getQualificationType()); + assertEquals("Q0001", driver.getQualificationNo()); + assertEquals("http://example.com/q-front.jpg", driver.getQualificationFront()); + assertEquals("http://example.com/q-back.jpg", driver.getQualificationBack()); + } + + @Test + @DisplayName("审核通过:不覆盖姓名与身份证号(身份标识由管理端建档确定)") + void approveDoesNotOverwriteIdentity() { + driver.setDriverName("张三(档案名)"); + driver.setIdCardNo("110101199001011234"); + + Long recordId = driverCertService.submitPersonCert(personCert("张三改名", "110101199001011234")); + certAuditService.audit(recordId, true, null); + + assertEquals("张三(档案名)", driver.getDriverName(), + "姓名是建档时确定的身份字段,司机端提交不得把它改掉"); + assertEquals("110101199001011234", driver.getIdCardNo()); + } + + @Test + @DisplayName("驳回:证件内容一个字都不写回档案") + void rejectWritesNothingBack() { + driver.setHeadPhoto("http://example.com/original-head.jpg"); + + PersonCertSubmitRequest request = personCert("张三", "110101199001011234"); + request.setHeadPhoto("http://example.com/new-head.jpg"); + Long recordId = driverCertService.submitPersonCert(request); + certAuditService.audit(recordId, false, "证件照片模糊"); + + assertEquals("http://example.com/original-head.jpg", driver.getHeadPhoto(), + "驳回只终结该次提交,档案内容不得被改动"); + } + + @Test + @DisplayName("历史快照:读不懂就跳过写回,审核本身照常通过") + void legacySnapshotSkipsWriteBackWithoutFailingAudit() { + // 改造前的快照没有 idCardNo / driverFileNo,解析出来字段全是 null; + // 此时既不能把 null 当成「司机没填」写进档案,也不能让这次审核直接失败 + Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234")); + TransportCertRecord record = recordStore.get(recordId); + record.setSnapshotJson("{\"legacyField\":\"改造前的旧内容\"}"); + + certAuditService.audit(recordId, true, null); + + assertEquals(TransportCertRecord.STATUS_APPROVED, recordStore.get(recordId).getStatus(), + "快照读不懂不该阻断审核"); + assertEquals(recordId, driver.getEffectiveCertRecordId(), "指针仍应照常切换"); + } }