From df3b21922db5df5587ffa5e7e70e3e48dafbd81d Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Wed, 28 Oct 2026 02:25:02 +0800 Subject: [PATCH] =?UTF-8?q?:recycle:=20=E5=AE=A1=E6=A0=B8=E4=BF=AE?= =?UTF-8?q?=E8=AE=A2=EF=BC=9A=E8=A1=A5=E8=BA=AB=E4=BB=BD=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=A0=A1=E9=AA=8C=E5=B9=B6=E6=95=B4?= =?UTF-8?q?=E7=90=86=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修订通过时比对原提交的身份字段(司机身份证号/驾驶证号、车牌号),原提交为空时允许补全;抽出 preferBody 与修订解析方法,统一许可头与注释口径。 --- .../transport/pojo/dto/CertAuditRequest.java | 4 +- .../CertificationAuditController.java | 30 ++++- .../service/impl/CertAuditServiceImpl.java | 120 +++++++++++++++--- .../support/CertificationRuleSupport.java | 18 +-- .../CertificationAuditEndpointTest.java | 28 +++- .../impl/CertAuditServiceImplTest.java | 53 ++++++++ 6 files changed, 213 insertions(+), 40 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/CertAuditRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/CertAuditRequest.java index e34302f6..00a69ce8 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/CertAuditRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/CertAuditRequest.java @@ -34,9 +34,7 @@ import java.io.Serializable; /** * 管理端认证审核请求 *

- * 这个接口原本只有 query 参数({@code id} / {@code pass} / {@code rejectReason}), - * 新增修订能力后改为「body 优先、query 兜底」:老前端不改代码仍然能用, - * 新前端把修订内容放进 body。两者同时给出时以 body 为准,避免审核员的修订被旧值覆盖。 + * 同一字段 body 与 query 都可传,两处都给时以 body 为准。 * * @author Chill */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CertificationAuditController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CertificationAuditController.java index aed2c2da..77c50af3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CertificationAuditController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CertificationAuditController.java @@ -49,6 +49,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.function.Function; + /** * 认证审核(管理端) *

@@ -102,12 +104,11 @@ public class CertificationAuditController extends BladeController { @Parameter(description = "认证记录主键(兼容老调用)") @RequestParam(required = false) Long id, @Parameter(description = "是否通过(兼容老调用)") @RequestParam(required = false) Boolean pass, @Parameter(description = "驳回原因,驳回时必填") @RequestParam(required = false) String rejectReason) { - // 原地升级:老前端只发 query,新前端发 body。逐字段「body 优先、query 兜底」—— - // 两者都给时以 body 为准,否则审核员在表单里改的内容会被 URL 上的旧值悄悄覆盖。 - Long recordId = body != null && body.getId() != null ? body.getId() : id; - Boolean approved = body != null && body.getPass() != null ? body.getPass() : pass; - String reason = body != null && body.getRejectReason() != null ? body.getRejectReason() : rejectReason; - String revision = body == null ? null : body.getAuditedSnapshotJson(); + // 同一字段 body 与 query 都可传,两处都给时以 body 为准。 + Long recordId = preferBody(body, CertAuditRequest::getId, id); + Boolean approved = preferBody(body, CertAuditRequest::getPass, pass); + String reason = preferBody(body, CertAuditRequest::getRejectReason, rejectReason); + String revision = preferBody(body, CertAuditRequest::getAuditedSnapshotJson, null); if (recordId == null) { return R.fail("缺少认证记录ID"); } @@ -125,4 +126,21 @@ public class CertificationAuditController extends BladeController { return R.data(certAuditService.capability()); } + /** + * 取一个审核参数:body 里给了就用 body 的,否则用 query 的 + * + * @param body 审核请求体,可为 null + * @param getter 从请求体取该字段 + * @param queryValue 该字段的 query 参数值 + * @param 字段类型 + * @return body 字段值;body 为 null 或该字段为 null 时返回 query 参数值 + */ + private static T preferBody(CertAuditRequest body, Function getter, T queryValue) { + if (body == null) { + return queryValue; + } + T bodyValue = getter.apply(body); + return bodyValue == null ? queryValue : bodyValue; + } + } 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 b6a3a8fe..91597a88 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 @@ -26,6 +26,7 @@ package org.springblade.transport.service.impl; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; @@ -172,9 +173,13 @@ public class CertAuditServiceImpl implements ICertAuditService { // 修订版只有「通过」才可能生效:驳回不产生生效内容,传了也不该留痕, // 否则历史里会出现一条「已驳回却带着生效内容」的记录。 String revision = pass ? blankToNull(auditedSnapshotJson) : null; + // 校验放在写库之前——内容不可用或改动了身份字段就整体回滚,不能先落一条状态再报错。 + if (revision != null) { + requireUsableRevision(record, revision); + requireIdentityUnchanged(record, revision); + } // 写回档案的是「本次生效内容」:有修订用修订版,没有就用司机原提交。 - // 校验放在写库之前——格式不对就整体回滚,不能先落一条状态再报错。 - String effectiveSnapshotJson = revision == null ? record.getSnapshotJson() : requireUsableRevision(record, revision); + String effectiveSnapshotJson = revision == null ? record.getSnapshotJson() : revision; Long auditor = AuthUtil.getUserId(); LocalDateTime auditTime = LocalDateTime.now(); @@ -347,7 +352,7 @@ public class CertAuditServiceImpl implements ICertAuditService { } /** - * 校验审核员填写的修订版内容,通过后原样返回(作为本次生效内容) + * 校验审核员填写的修订版内容,不满足时抛错 *

* 修订版是审核员的「最终认定」,会直接写回档案。因此必须能反序列化成当前提交结构, * 且带上判定「这份内容有效」的关键字段,否则写回等于什么也没写: @@ -358,33 +363,108 @@ public class CertAuditServiceImpl implements ICertAuditService { * * @param record 被审核的记录 * @param revision 已去空白的修订版内容 - * @return 同一个修订版内容 */ - private String requireUsableRevision(TransportCertRecord record, String revision) { + private void requireUsableRevision(TransportCertRecord record, String revision) { + JSONObject parsed = parseRevision(revision); if (TransportCertRecord.BIZ_TYPE_VEHICLE.equals(record.getBizType())) { - VehicleCertSubmitRequest snapshot; - try { - snapshot = JSON.parseObject(revision, VehicleCertSubmitRequest.class); - } catch (Exception exception) { - throw new ServiceException("修订内容格式不正确,请检查后重试"); - } + VehicleCertSubmitRequest snapshot = toSnapshot(parsed, VehicleCertSubmitRequest.class); if (snapshot == null || (Func.isEmpty(snapshot.getDrivingLicenseNo()) && Func.isEmpty(snapshot.getApprovedLoadKg()))) { throw new ServiceException("修订内容缺少行驶证号与核定载质量,无法写入车辆档案"); } - return revision; - } - PersonCertSubmitRequest snapshot; - try { - snapshot = JSON.parseObject(revision, PersonCertSubmitRequest.class); - } catch (Exception exception) { - throw new ServiceException("修订内容格式不正确,请检查后重试"); + return; } + PersonCertSubmitRequest snapshot = toSnapshot(parsed, PersonCertSubmitRequest.class); if (snapshot == null || (Func.isEmpty(snapshot.getIdCardNo()) && Func.isEmpty(snapshot.getDriverFileNo()))) { throw new ServiceException("修订内容缺少身份证号与驾驶证号,无法写入司机档案"); } - return revision; + } + + /** + * 身份字段不可修订:修订版改动了身份证号、驾驶证号(司机)或车牌号(车辆)时拒绝 + *

+ * 审核只认定证件内容,身份字段标识「是谁」,改了审核的就是另一个对象。 + * 原提交里该字段为空时(旧数据、空快照)没有可比对的原值,不比对,允许补全。 + * + * @param record 被审核的记录 + * @param revision 已去空白的修订版内容 + */ + private void requireIdentityUnchanged(TransportCertRecord record, String revision) { + JSONObject parsed = parseRevision(revision); + if (parsed == null) { + return; + } + if (TransportCertRecord.BIZ_TYPE_VEHICLE.equals(record.getBizType())) { + VehicleCertSubmitRequest original = parseSnapshotQuietly(record.getSnapshotJson(), VehicleCertSubmitRequest.class); + if (original == null) { + return; + } + VehicleCertSubmitRequest revised = toSnapshot(parsed, VehicleCertSubmitRequest.class); + requireSameIdentity(original.getPlateNo(), revised.getPlateNo(), "修订内容不能修改车牌号"); + return; + } + PersonCertSubmitRequest original = parseSnapshotQuietly(record.getSnapshotJson(), PersonCertSubmitRequest.class); + if (original == null) { + return; + } + String message = "修订内容不能修改身份证号与驾驶证号"; + PersonCertSubmitRequest revised = toSnapshot(parsed, PersonCertSubmitRequest.class); + requireSameIdentity(original.getIdCardNo(), revised.getIdCardNo(), message); + requireSameIdentity(original.getDriverFileNo(), revised.getDriverFileNo(), message); + } + + /** + * 原提交有值时,修订版必须与之一致;原提交为空时允许补全 + * + * @param original 原提交里的身份字段值 + * @param revised 修订版里的身份字段值 + * @param message 不一致时抛出的文案 + */ + private void requireSameIdentity(String original, String revised, String message) { + if (Func.isEmpty(original)) { + return; + } + if (!original.equals(revised)) { + throw new ServiceException(message); + } + } + + /** + * 反序列化审核员填写的修订版 JSON + * + * @param revision 已去空白的修订版内容 + * @return 解析结果;内容为字面量 null 时返回 null + */ + private JSONObject parseRevision(String revision) { + try { + return JSON.parseObject(revision); + } catch (Exception exception) { + throw new ServiceException("修订内容格式不正确,请检查后重试"); + } + } + + /** + * 解析原提交快照;为空或无法解析时返回 null(此时没有可比对的原值) + * + * @param snapshotJson 原提交内容 + * @param clazz 目标结构 + * @param 提交结构类型 + * @return 解析结果;为空或解析失败时返回 null + */ + private T parseSnapshotQuietly(String snapshotJson, Class clazz) { + if (Func.isEmpty(snapshotJson)) { + return null; + } + try { + return JSON.parseObject(snapshotJson, clazz); + } catch (Exception exception) { + return null; + } + } + + private T toSnapshot(JSONObject parsed, Class clazz) { + return parsed == null ? null : parsed.toJavaObject(clazz); } /** @@ -402,7 +482,7 @@ public class CertAuditServiceImpl implements ICertAuditService { *

* 快照缺失、无法解析或不是当前字段定义时**直接拒绝本次通过并整体回滚**: * 「通过」的语义是「把这批证件内容认定为生效值」,没有可用内容却放行, - * 等于给一条空记录盖上已通过的章——状态与内容不一致正是这次改造要消灭的。 + * 等于给一条空记录盖上已通过的章,状态与内容互相矛盾。 * 内容确实为空的历史记录,由审核员在审核时补全后重审即可。 * 申报的身份证号已落在另一名司机名下时同样阻断——那会把两条档案写成同一个证件号。 * diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertificationRuleSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertificationRuleSupport.java index b6831191..d0566a01 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertificationRuleSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/CertificationRuleSupport.java @@ -81,15 +81,6 @@ public final class CertificationRuleSupport { return previousPending; } - /** - * 判定认证对象是否已认证 - *

- * 唯一判据是生效快照指针。指针一旦写入永不回退,因此「曾通过即永久保持」, - * 重新提交、待审、被驳回都不会让对象变回未认证。 - * - * @param effectiveRecordId 生效快照指针 - * @return 是否已认证 - */ /** * 取一条记录的「当前生效内容」 *

@@ -111,6 +102,15 @@ public final class CertificationRuleSupport { : record.getSnapshotJson(); } + /** + * 判定认证对象是否已认证 + *

+ * 唯一判据是生效快照指针。指针一旦写入永不回退,因此「曾通过即永久保持」, + * 重新提交、待审、被驳回都不会让对象变回未认证。 + * + * @param effectiveRecordId 生效快照指针 + * @return 是否已认证 + */ public static boolean isCertified(Long effectiveRecordId) { return Func.isNotEmpty(effectiveRecordId); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/controller/CertificationAuditEndpointTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/controller/CertificationAuditEndpointTest.java index 6b462d7f..b4127ea1 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/controller/CertificationAuditEndpointTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/controller/CertificationAuditEndpointTest.java @@ -1,3 +1,28 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * 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.controller; import org.junit.jupiter.api.BeforeEach; @@ -22,8 +47,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. /** * 认证审核接口的入参绑定测试。 *

- * 这个接口是「原地升级」的:老前端只发 query 参数,新前端发 JSON body 带修订内容。 - * 两种调用都必须能用,且 body 一旦给了就以 body 为准——否则审核员的修订会被 query 里的旧值覆盖。 + * 同一字段 body 与 query 都可传,两处都给时以 body 为准。 */ class CertificationAuditEndpointTest { 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 a38af5d7..942e7f97 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 @@ -699,6 +699,59 @@ class CertAuditServiceImplTest { "生效一侧也修订版优先,否则审核页拿旧值做对比"); } + @Test + @DisplayName("身份不可修订:司机修订改了身份证号时拒绝,记录不被改动") + void approveRejectsDriverIdentityChange() { + when(certRecordService.getById(RECORD_ID)).thenReturn(driverRecord(TransportCertRecord.STATUS_PENDING)); + when(driverService.getById(DRIVER_ID)).thenReturn(driver(null)); + String revision = "{\"realName\":\"李四\",\"idCardNo\":\"450100199001019999\"," + + "\"driverFileNo\":\"450100888888\"}"; + + ServiceException exception = assertThrows(ServiceException.class, + () -> service.audit(RECORD_ID, true, null, revision)); + + assertTrue(exception.getMessage().contains("身份证号"), exception.getMessage()); + verify(driverService, never()).updateById(any(Driver.class)); + verify(certRecordService, never()).updateById(any(TransportCertRecord.class)); + } + + @Test + @DisplayName("身份不可修订:车辆修订改了车牌号时拒绝(车牌不参与写回,也不能被悄悄换掉)") + void approveRejectsVehicleIdentityChange() { + String snapshot = "{\"plateNo\":\"桂A32523\",\"drivingLicenseNo\":\"450100123456\"," + + "\"approvedLoadKg\":18000,\"ownerName\":\"广西现代物流集团有限公司\"}"; + when(certRecordService.getById(RECORD_ID)) + .thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING, snapshot)); + when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null)); + String revision = "{\"plateNo\":\"桂A99999\",\"drivingLicenseNo\":\"450100999999\"," + + "\"approvedLoadKg\":25000}"; + + ServiceException exception = assertThrows(ServiceException.class, + () -> service.audit(RECORD_ID, true, null, revision)); + + assertTrue(exception.getMessage().contains("车牌号"), exception.getMessage()); + verify(transportVehicleService, never()).updateById(any(TransportVehicle.class)); + verify(certRecordService, never()).updateById(any(TransportCertRecord.class)); + } + + @Test + @DisplayName("身份不可修订:原提交该字段为空时允许补全,正常通过") + void approveAllowsCompletingEmptyIdentity() { + TransportCertRecord record = driverRecord(TransportCertRecord.STATUS_PENDING); + record.setSnapshotJson("{\"realName\":\"张三\"}"); + when(certRecordService.getById(RECORD_ID)).thenReturn(record); + when(driverService.getById(DRIVER_ID)).thenReturn(driver(null)); + String revision = "{\"realName\":\"张三\",\"idCardNo\":\"450100199001011234\"," + + "\"driverFileNo\":\"450100888888\"}"; + + service.audit(RECORD_ID, true, null, revision); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Driver.class); + verify(driverService).updateById(captor.capture()); + assertEquals("450100199001011234", captor.getValue().getIdCardNo(), + "原提交没有身份证号时修订版可以补上"); + } + private static String vehicleRevision() { return "{\"drivingLicenseNo\":\"450100999999\",\"approvedLoadKg\":25000," + "\"ownerName\":\"修订后的所有人\"}";