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 new file mode 100644 index 00000000..00a69ce8 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/CertAuditRequest.java @@ -0,0 +1,62 @@ +/** + * 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.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 管理端认证审核请求 + *
+ * 同一字段 body 与 query 都可传,两处都给时以 body 为准。 + * + * @author Chill + */ +@Data +@Schema(description = "管理端认证审核请求") +public class CertAuditRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "认证记录主键") + private Long id; + @Schema(description = "true 通过,false 驳回") + private Boolean pass; + @Schema(description = "驳回原因;驳回时必填,≤200 字") + private String rejectReason; + /** + * 审核员修订后的全量认证内容(JSON);空或空白表示不修订 + *
+ * 通过时才生效:它既是写回档案的依据,也会原样留痕到认证记录上。 + */ + @Schema(description = "审核员修订后的全量认证内容JSON;空表示不修订") + private String auditedSnapshotJson; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportCertRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportCertRecord.java index 4f1346f4..bfed3f33 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportCertRecord.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportCertRecord.java @@ -28,6 +28,7 @@ package org.springblade.transport.pojo.entity; import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import io.swagger.v3.oas.annotations.media.Schema; @@ -125,6 +126,19 @@ public class TransportCertRecord extends TenantEntity { @TableField(updateStrategy = FieldStrategy.NEVER) @Schema(description = "该次提交的全量认证内容JSON") private String snapshotJson; + /** + * 审核通过时确定的修订版内容(JSON) + *
+ * 审核员可以在通过时直接改内容:司机提交的是 {@code snapshotJson}(永不可改), + * 最终写回档案、司机端读到的「当前生效内容」以本字段优先,为 null 才回退到 + * {@code snapshotJson}。为 null 即「审核员没有修订」。 + *
+ * {@code @JsonIgnore}:司机端的 {@code TransportCertRecordVO} 继承本实体, + * 修订版是审核过程的留痕,不该下发给司机。 + */ + @JsonIgnore + @Schema(description = "审核通过时确定的修订版内容JSON;null 表示未修订") + private String auditedSnapshotJson; /** * 驳回原因,仅当审核状态为已驳回时有值 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditDetailVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditDetailVO.java index 212eb072..672a7826 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditDetailVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CertAuditDetailVO.java @@ -81,6 +81,13 @@ public class CertAuditDetailVO implements Serializable { @Schema(description = "待审快照:本次提交的全量内容") private String pendingSnapshotJson; + /** + * 审核员在本次审核中填写的修订版内容;未修订时为 null + *
+ * 只在「通过」时才会落库,驳回不产生生效内容。审核页据此回显审核员改了什么。 + */ + @Schema(description = "本次审核提交的修订版内容;未修订时为 null") + private String auditedSnapshotJson; @Schema(description = "生效快照:该对象当前生效的全量内容;从未认证通过时为 null") private String effectiveSnapshotJson; @Schema(description = "生效快照记录ID;null 表示该对象从未认证通过") diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java index c4bf8a40..0983df03 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportCertRecordVO.java @@ -96,5 +96,13 @@ public class TransportCertRecordVO extends TransportCertRecord { @TableField(exist = false) @Schema(description = "司机手机号") private String driverMobile; + /** + * 该条记录是否经过审核员修订 + *
+ * 由服务层按 {@code auditedSnapshotJson} 是否有内容算出,前端据此打「已修订」标记。 + */ + @TableField(exist = false) + @Schema(description = "是否经过审核员修订") + private Boolean revised; } 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 6e6a024f..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 @@ -36,6 +36,7 @@ import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.CertAuditRequest; import org.springblade.transport.pojo.vo.CertAuditCapabilityVO; import org.springblade.transport.pojo.vo.CertAuditDetailVO; import org.springblade.transport.pojo.vo.TransportCertRecordVO; @@ -43,10 +44,13 @@ import org.springblade.transport.service.ICertAuditService; import org.springblade.transport.support.CertAuditPermissions; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; 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; + /** * 认证审核(管理端) *
@@ -92,13 +96,26 @@ public class CertificationAuditController extends BladeController {
@PostMapping("/audit")
@ApiOperationSupport(order = 3)
- @Operation(summary = "审核认证记录", description = "通过使该记录成为生效快照;驳回只终结该记录,生效快照不变")
+ @Operation(summary = "审核认证记录", description = "通过使该记录成为生效快照;驳回只终结该记录,生效快照不变。通过时可携带修订后的全量认证内容,它将成为新的生效内容并原样留痕")
@PreAuth(CertAuditPermissions.AUDIT_ACTION)
public R
+ * 修订版是审核员的「最终认定」,会直接写回档案。因此必须能反序列化成当前提交结构,
+ * 且带上判定「这份内容有效」的关键字段,否则写回等于什么也没写:
+ * 司机看身份证号与驾驶证号,车辆看行驶证号与核定载质量。
+ *
+ * 不满足时抛错、整体回滚——审核员填错 JSON 是操作失误,不能悄悄降级成「不修订」,
+ * 那样他会以为自己的修订已经生效。
+ *
+ * @param record 被审核的记录
+ * @param revision 已去空白的修订版内容
+ */
+ private void requireUsableRevision(TransportCertRecord record, String revision) {
+ JSONObject parsed = parseRevision(revision);
+ if (TransportCertRecord.BIZ_TYPE_VEHICLE.equals(record.getBizType())) {
+ VehicleCertSubmitRequest snapshot = toSnapshot(parsed, VehicleCertSubmitRequest.class);
+ if (snapshot == null
+ || (Func.isEmpty(snapshot.getDrivingLicenseNo()) && Func.isEmpty(snapshot.getApprovedLoadKg()))) {
+ throw new ServiceException("修订内容缺少行驶证号与核定载质量,无法写入车辆档案");
+ }
+ return;
+ }
+ PersonCertSubmitRequest snapshot = toSnapshot(parsed, PersonCertSubmitRequest.class);
+ if (snapshot == null
+ || (Func.isEmpty(snapshot.getIdCardNo()) && Func.isEmpty(snapshot.getDriverFileNo()))) {
+ throw new ServiceException("修订内容缺少身份证号与驾驶证号,无法写入司机档案");
+ }
+ }
+
+ /**
+ * 身份字段不可修订:修订版改动了身份证号、驾驶证号(司机)或车牌号(车辆)时拒绝
+ *
+ * 审核只认定证件内容,身份字段标识「是谁」,改了审核的就是另一个对象。
+ * 原提交里该字段为空时(旧数据、空快照)没有可比对的原值,不比对,允许补全。
+ *
+ * @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
@@ -345,33 +480,34 @@ public class CertAuditServiceImpl implements ICertAuditService {
* 快照里为 null 的字段不会写(框架的 not_null 更新策略会跳过),
* 因此不会把管理端填过、司机端没填的字段清空。
*
- * 快照缺失、无法解析或不是当前字段定义时**只跳过写回,不阻断审核**:
- * 审核本身是独立于写回的业务动作(切指针、置状态),不该因为一份读不懂的快照而失败;
- * 何况历史快照本来就存在,阻断会让这些对象再也审不过去。
- * 申报的身份证号已落在另一名司机名下时是例外——那会把两条档案写成同一个证件号,
- * 必须阻断。
+ * 快照缺失、无法解析或不是当前字段定义时**直接拒绝本次通过并整体回滚**:
+ * 「通过」的语义是「把这批证件内容认定为生效值」,没有可用内容却放行,
+ * 等于给一条空记录盖上已通过的章,状态与内容互相矛盾。
+ * 内容确实为空的历史记录,由审核员在审核时补全后重审即可。
+ * 申报的身份证号已落在另一名司机名下时同样阻断——那会把两条档案写成同一个证件号。
*
* @param driver 待更新的司机 patch
* @param snapshotJson 该条记录的全量提交内容
*/
private void applySnapshotToDriver(Driver driver, String snapshotJson) {
if (Func.isEmpty(snapshotJson)) {
- return;
+ throw new ServiceException("该认证记录没有提交内容,无法写入司机档案;请让司机重新提交一次认证");
}
PersonCertSubmitRequest snapshot;
try {
snapshot = JSON.parseObject(snapshotJson, PersonCertSubmitRequest.class);
} catch (Exception exception) {
- return;
+ throw new ServiceException("该认证记录的提交内容无法解析,未写入司机档案;请重新提交一次认证");
}
if (snapshot == null) {
- return;
+ throw new ServiceException("该认证记录的提交内容无法解析,未写入司机档案;请重新提交一次认证");
}
// 字段名对不上时 JSON 会解析成「全是 null」而不报错(历史快照就是这种情况),
// 那样写回等于什么也没写,还会把档案里已有的值当成「没填」而跳过。
- // 故用证件内容里的关键字段判定「这份快照是否是当前格式」,不是就不写。
+ // 故用证件内容里的关键字段判定「这份快照是否是当前格式」,不是就拒绝。
if (Func.isEmpty(snapshot.getIdCardNo()) && Func.isEmpty(snapshot.getDriverFileNo())) {
- return;
+ throw new ServiceException("该认证记录的提交内容与当前字段定义不匹配(可能是早期格式的历史快照),"
+ + "未写入司机档案;请重新提交一次认证");
}
// 身份证号要落到档案上,先确认它没有落在别人名下:重复证件号一旦落库,
// 建档查重与按证件号识别同一人都会失效
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
index 703119df..82e05905 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
@@ -209,9 +209,11 @@ public class DriverCertServiceImpl implements IDriverCertService {
Driver driver = requireCurrentDriver();
// 当前司机实体已加载过,直接读它身上的指针,不再回查一次
TransportCertRecord effective = findEffectiveRecord(driver.getEffectiveCertRecordId());
+ // 生效内容以修订版为准:审核员改过的值才是档案里真正生效的那份,
+ // 仍读 snapshot_json 会让司机端拿旧值覆盖审核员的改动
return effective == null
? null
- : JSON.parseObject(effective.getSnapshotJson(), PersonCertSubmitRequest.class);
+ : JSON.parseObject(CertificationRuleSupport.effectiveSnapshotJson(effective), PersonCertSubmitRequest.class);
}
@Override
@@ -272,7 +274,7 @@ public class DriverCertServiceImpl implements IDriverCertService {
TransportCertRecord effective = findEffectiveRecord(vehicle.getEffectiveCertRecordId());
return effective == null
? null
- : JSON.parseObject(effective.getSnapshotJson(), VehicleCertSubmitRequest.class);
+ : JSON.parseObject(CertificationRuleSupport.effectiveSnapshotJson(effective), VehicleCertSubmitRequest.class);
}
@Override
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 7fc8ce28..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,6 +81,27 @@ public final class CertificationRuleSupport {
return previousPending;
}
+ /**
+ * 取一条记录的「当前生效内容」
+ *
+ * 审核员可以在通过时修订内容:修订版存在则以修订版为准,否则回退到司机原提交
+ * ({@code snapshot_json} 永不修改,它是司机当时提交了什么的历史证据)。
+ *
+ * 写回档案、司机端读生效值、审核详情对比三处必须用同一套取法,
+ * 否则会出现「档案是新值、司机端还是旧值」这种自相矛盾。
+ *
+ * @param record 认证记录,可为 null
+ * @return 生效内容 JSON;记录为空、或两者都为空时返回 null
+ */
+ public static String effectiveSnapshotJson(TransportCertRecord record) {
+ if (record == null) {
+ return null;
+ }
+ return Func.isNotBlank(record.getAuditedSnapshotJson())
+ ? record.getAuditedSnapshotJson()
+ : record.getSnapshotJson();
+ }
+
/**
* 判定认证对象是否已认证
*
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java
index f6a8d7e2..fdd8994d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportCertRecordWrapper.java
@@ -27,6 +27,7 @@ package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
+import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.system.cache.UserDisplayCache;
import org.springblade.transport.pojo.entity.TransportCertRecord;
@@ -70,6 +71,8 @@ public class TransportCertRecordWrapper extends BaseEntityWrapper
+ * 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;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springblade.transport.service.ICertAuditService;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * 认证审核接口的入参绑定测试。
+ *
+ * 同一字段 body 与 query 都可传,两处都给时以 body 为准。
+ */
+class CertificationAuditEndpointTest {
+
+ private static final Long RECORD_ID = 5001L;
+ private static final String REVISION = "{\"realName\":\"李四\",\"idCardNo\":\"450100199001019999\"}";
+
+ private ICertAuditService certAuditService;
+ private MockMvc mockMvc;
+
+ @BeforeEach
+ void setUp() {
+ certAuditService = mock(ICertAuditService.class);
+ when(certAuditService.audit(any(), anyBoolean(), any(), any())).thenReturn(true);
+ mockMvc = MockMvcBuilders.standaloneSetup(new CertificationAuditController(certAuditService)).build();
+ }
+
+ @Test
+ @DisplayName("老调用:只带 query 的 id/pass/rejectReason,行为不变")
+ void legacyQueryOnlyStillWorks() throws Exception {
+ mockMvc.perform(post("/certification-audit/audit")
+ .param("id", String.valueOf(RECORD_ID))
+ .param("pass", "true"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+
+ verify(certAuditService).audit(RECORD_ID, true, null, null);
+ }
+
+ @Test
+ @DisplayName("新调用:body 携带修订内容,透传到服务层")
+ void bodyCarriesRevision() throws Exception {
+ String body = """
+ {"id": 5001, "pass": true, "auditedSnapshotJson": "{\\"realName\\":\\"李四\\",\\"idCardNo\\":\\"450100199001019999\\"}"}
+ """;
+
+ mockMvc.perform(post("/certification-audit/audit")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(body))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+
+ verify(certAuditService).audit(RECORD_ID, true, null, REVISION);
+ }
+
+ @Test
+ @DisplayName("两种都带:以 body 为准(query 里的旧值不得覆盖修订)")
+ void bodyWinsOverQuery() throws Exception {
+ String body = """
+ {"id": 5001, "pass": true}
+ """;
+
+ mockMvc.perform(post("/certification-audit/audit")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(body)
+ .param("id", "6001")
+ .param("pass", "false"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+
+ verify(certAuditService).audit(RECORD_ID, true, null, null);
+ }
+
+ @Test
+ @DisplayName("body 只给一半时,缺的字段回退到 query")
+ void partialBodyFallsBackToQuery() throws Exception {
+ String body = """
+ {"id": 5001, "pass": false}
+ """;
+
+ mockMvc.perform(post("/certification-audit/audit")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(body)
+ .param("rejectReason", "行驶证模糊"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+
+ verify(certAuditService).audit(RECORD_ID, false, "行驶证模糊", null);
+ }
+
+ @Test
+ @DisplayName("缺少 id 或 pass:按业务失败返回(code=400),不能把 null 传进服务层")
+ void missingIdOrPassIsRejected() throws Exception {
+ mockMvc.perform(post("/certification-audit/audit").param("pass", "true"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(400));
+
+ mockMvc.perform(post("/certification-audit/audit").param("id", String.valueOf(RECORD_ID)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(400));
+
+ verify(certAuditService, never()).audit(anyLong(), anyBoolean(), any(), any());
+ }
+}
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 fa461766..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
@@ -96,7 +96,7 @@ class CertAuditServiceImplTest {
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
- service.audit(RECORD_ID, true, null);
+ service.audit(RECORD_ID, true, null, null);
ArgumentCaptor
+ * 司机侧的空快照不再「静默跳过」而是拒审,故通过类用例不能再拿 "{}" 走流程;
+ * 身份证号与驾驶证号是「这份快照是否为当前格式」的判据,缺一不可。
+ */
+ static String completeDriverSnapshot() {
+ return "{\"realName\":\"张三\",\"idCardNo\":\"450100199001011234\","
+ + "\"driverFileNo\":\"450100888888\",\"driverClass\":\"A2\","
+ + "\"headPhoto\":\"http://oss/head.jpg\",\"idCardFrontUrl\":\"http://oss/idcard-front.jpg\","
+ + "\"idCardBackUrl\":\"http://oss/idcard-back.jpg\"}";
+ }
+
private TransportCertRecord driverRecord(Integer status) {
TransportCertRecord record = new TransportCertRecord();
record.setId(RECORD_ID);
@@ -563,7 +806,7 @@ class CertAuditServiceImplTest {
record.setBizId(DRIVER_ID);
record.setBizKey("张三");
record.setStatus(status);
- record.setSnapshotJson("{}");
+ record.setSnapshotJson(completeDriverSnapshot());
return record;
}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java
index 7e99a3e5..10b8e190 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverCertServiceImplTest.java
@@ -358,6 +358,56 @@ class DriverCertServiceImplTest {
assertNull(service.getEffectivePersonCert());
}
+ @Test
+ @DisplayName("读生效值:有修订版时返回修订版内容(司机端不能拿旧值覆盖审核员的改动)")
+ void getEffectivePersonCertPrefersRevision() {
+ Long effectiveId = 777L;
+ Driver driver = driver(PLATE);
+ driver.setEffectiveCertRecordId(effectiveId);
+ when(driverAppService.currentDriverEntity()).thenReturn(driver);
+ when(certRecordService.getById(effectiveId)).thenReturn(pointedRecord(effectiveId,
+ "司机原提交", "审核员修订"));
+
+ PersonCertSubmitRequest result = service.getEffectivePersonCert();
+
+ assertEquals("审核员修订", result.getRealName());
+ }
+
+ @Test
+ @DisplayName("读生效值:车辆同理,修订版优先")
+ void getEffectiveVehicleCertPrefersRevision() {
+ when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE));
+ when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, 777L));
+ when(certRecordService.getById(777L)).thenReturn(pointedVehicleRecord(777L,
+ "司机原提交", "审核员修订"));
+
+ VehicleCertSubmitRequest result = service.getEffectiveVehicleCert(VEHICLE_ID);
+
+ assertEquals("审核员修订", result.getOwnerName());
+ }
+
+ private TransportCertRecord pointedRecord(Long id, String submittedName, String revisedName) {
+ PersonCertSubmitRequest submitted = new PersonCertSubmitRequest();
+ submitted.setRealName(submittedName);
+ PersonCertSubmitRequest revised = new PersonCertSubmitRequest();
+ revised.setRealName(revisedName);
+ TransportCertRecord record = record(id, TransportCertRecord.STATUS_APPROVED);
+ record.setSnapshotJson(JSON.toJSONString(submitted));
+ record.setAuditedSnapshotJson(JSON.toJSONString(revised));
+ return record;
+ }
+
+ private TransportCertRecord pointedVehicleRecord(Long id, String submittedName, String revisedName) {
+ VehicleCertSubmitRequest submitted = new VehicleCertSubmitRequest();
+ submitted.setOwnerName(submittedName);
+ VehicleCertSubmitRequest revised = new VehicleCertSubmitRequest();
+ revised.setOwnerName(revisedName);
+ TransportCertRecord record = record(id, TransportCertRecord.STATUS_APPROVED);
+ record.setSnapshotJson(JSON.toJSONString(submitted));
+ record.setAuditedSnapshotJson(JSON.toJSONString(revised));
+ return record;
+ }
+
@Test
@DisplayName("重新提交后,生效值仍是旧的(核心验收点:提交不影响已认证状态)")
void resubmitKeepsEffectiveValue() {
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 0fee6807..d6e3c229 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
@@ -108,7 +108,7 @@ class PersonCertRoundTripTest {
void submitThenApprove() {
// 1) 先走一遍完整流程,让该司机有一个生效值
Long firstRecordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
- certAuditService.audit(firstRecordId, true, null);
+ certAuditService.audit(firstRecordId, true, null, null);
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
"审核通过后应能查到生效值");
@@ -120,7 +120,7 @@ class PersonCertRoundTripTest {
"指针应仍指向原来那条已通过记录");
// 3) 审核通过:生效值才变成新值
- certAuditService.audit(secondRecordId, true, null);
+ certAuditService.audit(secondRecordId, true, null, null);
assertEquals("张三改名", driverCertService.getEffectivePersonCert().getRealName(),
"审核通过后生效值应切到新记录");
assertEquals(secondRecordId, driver.getEffectiveCertRecordId());
@@ -130,10 +130,10 @@ class PersonCertRoundTripTest {
@DisplayName("驳回后生效值不变(已认证对象不会被驳回下线)")
void rejectKeepsEffectiveValue() {
Long firstRecordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
- certAuditService.audit(firstRecordId, true, null);
+ certAuditService.audit(firstRecordId, true, null, null);
Long secondRecordId = driverCertService.submitPersonCert(personCert("张三改名", "110101199001011234"));
- certAuditService.audit(secondRecordId, false, "证件照片模糊");
+ certAuditService.audit(secondRecordId, false, "证件照片模糊", null);
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
"驳回只终结该次提交,既得认证必须保持有效");
@@ -149,7 +149,7 @@ class PersonCertRoundTripTest {
assertNull(driverCertService.getEffectivePersonCert(),
"待审期间不应有生效值");
- certAuditService.audit(recordId, true, null);
+ certAuditService.audit(recordId, true, null, null);
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName());
}
@@ -171,18 +171,18 @@ class PersonCertRoundTripTest {
// 被作废的那条不能再被审核(否则会把已作废的记录变成生效值)
org.junit.jupiter.api.Assertions.assertThrows(
org.springblade.core.log.exception.ServiceException.class,
- () -> certAuditService.audit(first, true, null));
+ () -> certAuditService.audit(first, true, null, null));
}
@Test
@DisplayName("已通过的记录不可再被审核(防止把生效快照改坏)")
void approvedRecordCannotBeReaudited() {
Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
- certAuditService.audit(recordId, true, null);
+ certAuditService.audit(recordId, true, null, null);
org.junit.jupiter.api.Assertions.assertThrows(
org.springblade.core.log.exception.ServiceException.class,
- () -> certAuditService.audit(recordId, false, "事后反悔"));
+ () -> certAuditService.audit(recordId, false, "事后反悔", null));
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
"重复审核被拒后,生效值不受影响");
}
@@ -314,7 +314,7 @@ class PersonCertRoundTripTest {
request.setQualificationBack("http://example.com/q-back.jpg");
Long recordId = driverCertService.submitPersonCert(request);
- certAuditService.audit(recordId, true, null);
+ certAuditService.audit(recordId, true, null, null);
// 档案里必须是这次提交的内容:只切指针、不写内容的话,
// 认证状态显示「已通过」而档案仍是旧值,管理端与司机端会各看各的
@@ -342,7 +342,7 @@ class PersonCertRoundTripTest {
request.setAddressRegion(" ");
request.setAddress("");
Long recordId = driverCertService.submitPersonCert(request);
- certAuditService.audit(recordId, true, null);
+ certAuditService.audit(recordId, true, null, null);
// 框架的 not_null 更新策略只跳过 null:写空串会把管理端填过的住址清掉
assertEquals("广西壮族自治区柳州市柳北区", driver.getAddressRegion(),
@@ -358,7 +358,7 @@ class PersonCertRoundTripTest {
driver.setIdCardNo("110101199001011234");
Long recordId = driverCertService.submitPersonCert(personCert("张三改名", "110101199001011234"));
- certAuditService.audit(recordId, true, null);
+ certAuditService.audit(recordId, true, null, null);
// 审核通过即认可这次申报:档案里必须也是申报的姓名,
// 否则司机端卡片显示档案名、「重新认证」表单回填快照名,同一个页面上两个名字
@@ -377,7 +377,7 @@ class PersonCertRoundTripTest {
Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011999"));
ServiceException exception = assertThrows(ServiceException.class,
- () -> certAuditService.audit(recordId, true, null));
+ () -> certAuditService.audit(recordId, true, null, null));
assertTrue(exception.getMessage().contains("另一名司机档案"), exception.getMessage());
// 真实环境下这次审核由 @Transactional 整体回滚(记录仍是待审);
// 单测没有事务,故这里校验更直接的一点:拦截发生在写档案之前,别人的证件号没被写进来
@@ -392,25 +392,27 @@ class PersonCertRoundTripTest {
PersonCertSubmitRequest request = personCert("张三", "110101199001011234");
request.setHeadPhoto("http://example.com/new-head.jpg");
Long recordId = driverCertService.submitPersonCert(request);
- certAuditService.audit(recordId, false, "证件照片模糊");
+ certAuditService.audit(recordId, false, "证件照片模糊", null);
assertEquals("http://example.com/original-head.jpg", driver.getHeadPhoto(),
"驳回只终结该次提交,档案内容不得被改动");
}
@Test
- @DisplayName("历史快照:读不懂就跳过写回,审核本身照常通过")
- void legacySnapshotSkipsWriteBackWithoutFailingAudit() {
- // 早期格式的快照没有 idCardNo / driverFileNo,解析出来字段全是 null;
- // 此时既不能把 null 当成「司机没填」写进档案,也不能让这次审核直接失败
+ @DisplayName("历史快照:读不懂就拒绝通过并回滚(不再静默放行)")
+ void legacySnapshotBlocksAuditInsteadOfPassing() {
+ // 早期格式的快照没有 idCardNo / driverFileNo,解析出来字段全是 null。
+ // 「通过」的语义是「把这批内容认定为生效值」,内容读不懂就无从认定,
+ // 必须拒绝——静默放行等于给一条空记录盖上已通过的章。
Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
TransportCertRecord record = recordStore.get(recordId);
record.setSnapshotJson("{\"legacyField\":\"早期格式的旧内容\"}");
- certAuditService.audit(recordId, true, null);
+ ServiceException exception = assertThrows(ServiceException.class,
+ () -> certAuditService.audit(recordId, true, null, null));
- assertEquals(TransportCertRecord.STATUS_APPROVED, recordStore.get(recordId).getStatus(),
- "快照读不懂不该阻断审核");
- assertEquals(recordId, driver.getEffectiveCertRecordId(), "指针仍应照常切换");
+ assertTrue(exception.getMessage().contains("不匹配"), exception.getMessage());
+ // 内存版记录表没有事务,这里只验最关键的一条:生效指针不能被切过去
+ assertNull(driver.getEffectiveCertRecordId(), "审核失败不得切换生效指针");
}
}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/wrapper/TransportCertRecordWrapperRevisionTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/wrapper/TransportCertRecordWrapperRevisionTest.java
new file mode 100644
index 00000000..df025a94
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/wrapper/TransportCertRecordWrapperRevisionTest.java
@@ -0,0 +1,86 @@
+/**
+ * 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.wrapper;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.springblade.system.cache.UserCache;
+import org.springblade.system.cache.UserDisplayCache;
+import org.springblade.transport.pojo.entity.TransportCertRecord;
+import org.springblade.transport.pojo.vo.TransportCertRecordVO;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+/**
+ * 认证记录视图对象的修订版标记与字段可见性
+ *
+ * 两件事必须同时成立:司机端要能从 {@code revised} 看出「这条记录经过审核员修订」,
+ * 而又不能顺着 VO 把整份修订内容(含身份证号、证件照地址)再下发一遍。
+ *
+ * @author Chill
+ */
+class TransportCertRecordWrapperRevisionTest {
+
+ @Test
+ @DisplayName("修订版非空才算修订,空串与空白不算")
+ void marksRevisedOnlyWhenRevisionPresent() {
+ try (MockedStatic