♻️ 审核修订:补身份字段服务端校验并整理注释
修订通过时比对原提交的身份字段(司机身份证号/驾驶证号、车牌号),原提交为空时允许补全;抽出 preferBody 与修订解析方法,统一许可头与注释口径。
This commit is contained in:
+1
-3
@@ -34,9 +34,7 @@ import java.io.Serializable;
|
||||
/**
|
||||
* 管理端认证审核请求
|
||||
* <p>
|
||||
* 这个接口原本只有 query 参数({@code id} / {@code pass} / {@code rejectReason}),
|
||||
* 新增修订能力后改为「body 优先、query 兜底」:老前端不改代码仍然能用,
|
||||
* 新前端把修订内容放进 body。两者同时给出时以 body 为准,避免审核员的修订被旧值覆盖。
|
||||
* 同一字段 body 与 query 都可传,两处都给时以 body 为准。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
|
||||
+24
-6
@@ -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;
|
||||
|
||||
/**
|
||||
* 认证审核(管理端)
|
||||
* <p>
|
||||
@@ -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 <T> 字段类型
|
||||
* @return body 字段值;body 为 null 或该字段为 null 时返回 query 参数值
|
||||
*/
|
||||
private static <T> T preferBody(CertAuditRequest body, Function<CertAuditRequest, T> getter, T queryValue) {
|
||||
if (body == null) {
|
||||
return queryValue;
|
||||
}
|
||||
T bodyValue = getter.apply(body);
|
||||
return bodyValue == null ? queryValue : bodyValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+100
-20
@@ -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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验审核员填写的修订版内容,通过后原样返回(作为本次生效内容)
|
||||
* 校验审核员填写的修订版内容,不满足时抛错
|
||||
* <p>
|
||||
* 修订版是审核员的「最终认定」,会直接写回档案。因此必须能反序列化成当前提交结构,
|
||||
* 且带上判定「这份内容有效」的关键字段,否则写回等于什么也没写:
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份字段不可修订:修订版改动了身份证号、驾驶证号(司机)或车牌号(车辆)时拒绝
|
||||
* <p>
|
||||
* 审核只认定证件内容,身份字段标识「是谁」,改了审核的就是另一个对象。
|
||||
* 原提交里该字段为空时(旧数据、空快照)没有可比对的原值,不比对,允许补全。
|
||||
*
|
||||
* @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 <T> 提交结构类型
|
||||
* @return 解析结果;为空或解析失败时返回 null
|
||||
*/
|
||||
private <T> T parseSnapshotQuietly(String snapshotJson, Class<T> clazz) {
|
||||
if (Func.isEmpty(snapshotJson)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parseObject(snapshotJson, clazz);
|
||||
} catch (Exception exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T toSnapshot(JSONObject parsed, Class<T> clazz) {
|
||||
return parsed == null ? null : parsed.toJavaObject(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -402,7 +482,7 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
* <p>
|
||||
* 快照缺失、无法解析或不是当前字段定义时**直接拒绝本次通过并整体回滚**:
|
||||
* 「通过」的语义是「把这批证件内容认定为生效值」,没有可用内容却放行,
|
||||
* 等于给一条空记录盖上已通过的章——状态与内容不一致正是这次改造要消灭的。
|
||||
* 等于给一条空记录盖上已通过的章,状态与内容互相矛盾。
|
||||
* 内容确实为空的历史记录,由审核员在审核时补全后重审即可。
|
||||
* 申报的身份证号已落在另一名司机名下时同样阻断——那会把两条档案写成同一个证件号。
|
||||
*
|
||||
|
||||
+9
-9
@@ -81,15 +81,6 @@ public final class CertificationRuleSupport {
|
||||
return previousPending;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定认证对象是否已认证
|
||||
* <p>
|
||||
* 唯一判据是生效快照指针。指针一旦写入永不回退,因此「曾通过即永久保持」,
|
||||
* 重新提交、待审、被驳回都不会让对象变回未认证。
|
||||
*
|
||||
* @param effectiveRecordId 生效快照指针
|
||||
* @return 是否已认证
|
||||
*/
|
||||
/**
|
||||
* 取一条记录的「当前生效内容」
|
||||
* <p>
|
||||
@@ -111,6 +102,15 @@ public final class CertificationRuleSupport {
|
||||
: record.getSnapshotJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定认证对象是否已认证
|
||||
* <p>
|
||||
* 唯一判据是生效快照指针。指针一旦写入永不回退,因此「曾通过即永久保持」,
|
||||
* 重新提交、待审、被驳回都不会让对象变回未认证。
|
||||
*
|
||||
* @param effectiveRecordId 生效快照指针
|
||||
* @return 是否已认证
|
||||
*/
|
||||
public static boolean isCertified(Long effectiveRecordId) {
|
||||
return Func.isNotEmpty(effectiveRecordId);
|
||||
}
|
||||
|
||||
+26
-2
@@ -1,3 +1,28 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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.
|
||||
/**
|
||||
* 认证审核接口的入参绑定测试。
|
||||
* <p>
|
||||
* 这个接口是「原地升级」的:老前端只发 query 参数,新前端发 JSON body 带修订内容。
|
||||
* 两种调用都必须能用,且 body 一旦给了就以 body 为准——否则审核员的修订会被 query 里的旧值覆盖。
|
||||
* 同一字段 body 与 query 都可传,两处都给时以 body 为准。
|
||||
*/
|
||||
class CertificationAuditEndpointTest {
|
||||
|
||||
|
||||
+53
@@ -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<Driver> 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\":\"修订后的所有人\"}";
|
||||
|
||||
Reference in New Issue
Block a user