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
* 修订版是审核员的「最终认定」,会直接写回档案。因此必须能反序列化成当前提交结构,
* 且带上判定「这份内容有效」的关键字段,否则写回等于什么也没写:
@@ -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
* 快照缺失、无法解析或不是当前字段定义时**直接拒绝本次通过并整体回滚**:
* 「通过」的语义是「把这批证件内容认定为生效值」,没有可用内容却放行,
- * 等于给一条空记录盖上已通过的章——状态与内容不一致正是这次改造要消灭的。
+ * 等于给一条空记录盖上已通过的章,状态与内容互相矛盾。
* 内容确实为空的历史记录,由审核员在审核时补全后重审即可。
* 申报的身份证号已落在另一名司机名下时同样阻断——那会把两条档案写成同一个证件号。
*
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