✨ 新增认证记录不可变快照数据模型与迁移脚本

按 ADR-0001/0002 建立司机与车辆共用的认证记录表 blade_transport_cert_record
(biz_type 区分、载荷存 JSON、检索字段提升为真实列),并为司机/车辆表补生效快照指针。

不变式结构性保证:
- I1 同对象至多一条待审:生成列 pending_biz_id + 唯一索引,数据库层拦截;
  生成列同时排除 is_deleted,避免逻辑删除的待审记录永久占用唯一键。
- I3 快照不可变:snapshot_json 等 5 列标 FieldStrategy.NEVER,永不进入 UPDATE。
- status 承载审核状态(0审核中/1已通过/2已驳回/3已作废),新增时必须显式设置:
  框架 resolveEntity 会把 null 的 status 静默填成 1(即「已通过」),
  且继承来的 changeStatus 可直接写 status,两条通道均已封堵。

其他:司机表补身份证有效期三列并订正排序规则为 utf8mb4_general_ci
(原先为 0900_ai_ci,与车辆表 JOIN 已发生过 Illegal mix of collations);
生效快照指针加 @JsonProperty(READ_ONLY),防止以 /submit 请求体自行「认证」自己;
同步基线建表脚本与认证审核菜单/权限码。本票不含任何业务接口。
This commit is contained in:
2026-10-02 14:40:56 +08:00
parent 087f361a8b
commit 87f4c3a830
13 changed files with 852 additions and 4 deletions
@@ -28,6 +28,9 @@ 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.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -111,6 +114,21 @@ public class Driver extends TenantEntity {
*/
@Schema(description = "身份证反面照")
private String idCardBack;
/**
* 身份证有效期起
*/
@Schema(description = "身份证有效期起")
private LocalDate idCardValidStart;
/**
* 身份证有效期止
*/
@Schema(description = "身份证有效期止")
private LocalDate idCardValidEnd;
/**
* 身份证长期有效
*/
@Schema(description = "身份证长期有效")
private Integer idCardLongTerm;
/**
* 大头照
*/
@@ -221,5 +239,31 @@ public class Driver extends TenantEntity {
*/
@Schema(description = "备注")
private String remark;
/**
* 最新提交状态展示缓存:0审核中 1已通过 2已驳回 3已作废,null 表示从未提交
* <p>
* 仅为列表展示而冗余,<b>不是</b>可用性判据;「是否已认证」一律读生效快照指针。
*/
@Schema(description = "最新提交状态展示缓存:0审核中 1已通过 2已驳回 3已作废,null为从未提交")
private Integer certificationStatus;
/**
* 最新一次驳回原因展示缓存
*/
@Schema(description = "最新一次驳回原因展示缓存")
private String certificationRejectReason;
/**
* 生效快照指针:指向 blade_transport_cert_record.id
* <p>
* 「该司机是否已认证」的<b>唯一</b>判据。为 null 表示从未认证通过;
* 一经设置<b>永不回退</b>——提交、驳回、作废都不得修改它(见 ADR-0002)。
* <p>
* ⚠️ {@code READ_ONLY}:本字段<b>只能由审核链路</b>(03)写入。
* 司机档案的 {@code /submit} 直接以本实体接收请求体,若允许反序列化,
* 调用方就能自行填一个记录 ID 把自己「认证」掉,绕过审核与 D10 的可用性门槛。
*/
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "生效快照指针")
private Long effectiveCertRecordId;
}
@@ -0,0 +1,156 @@
/**
* 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.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 认证记录实体类
* <p>
* 认证对象(司机 / 车辆)<b>每一次提交</b>所产生的、<b>不可变</b>的快照。
* 记录一经创建,其 {@code snapshotJson} 与提交元信息永不修改;被同对象的后续提交取代时
* 标记为「已作废」,而<b>不是</b>删除。判定某个对象「是否已认证」只看它持有的生效快照指针,
* 与记录表里的任何一条记录的状态都无关。
* <p>
* 设计依据:{@code docs/adr/0001-certification-record-immutable-snapshot.md},
* 术语见根工作区 {@code GLOSSARY.md}。
* <p>
* ⚠️ <b>{@code status} 承载的是「审核状态」,不是 BladeX 的「启用 / 停用」。</b>
* 该字段继承自 {@link TenantEntity},带来两条必须堵住的框架通道:
* 一是 {@code BaseServiceImpl#resolveEntity} 在插入时会把 {@code null} 的 {@code status}
* <b>静默填成 {@code 1}</b>(恰好是 {@link #STATUS_APPROVED});
* 二是继承来的 {@code changeStatus(ids, 1)} 会直接写 {@code status},等于批量通过审核。
* 二者都已在 {@code TransportCertRecordServiceImpl} 中封堵(前者校验抛错、后者覆写抛错),
* 审核一律走 03 的审核接口。
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_transport_cert_record")
@Schema(description = "认证记录")
public class TransportCertRecord extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 认证对象类型:司机
*/
public static final String BIZ_TYPE_DRIVER = "driver";
/**
* 认证对象类型:车辆
*/
public static final String BIZ_TYPE_VEHICLE = "vehicle";
/**
* 审核状态:审核中(即该对象的「待审快照」)
*/
public static final Integer STATUS_PENDING = 0;
/**
* 审核状态:已通过(该对象最新一条已通过者即其「生效快照」)
*/
public static final Integer STATUS_APPROVED = 1;
/**
* 审核状态:已驳回(审核的否定结论,需带驳回原因)
*/
public static final Integer STATUS_REJECTED = 2;
/**
* 审核状态:已作废(被同对象的再次提交取代,非驳回)
*/
public static final Integer STATUS_SUPERSEDED = 3;
/**
* 认证对象类型:driver-司机,vehicle-车辆
* <p>
* {@code NEVER}:记录一经创建,认证对象的身份不可改(详见类注释的不变式 I3)。
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@Schema(description = "认证对象类型:driver-司机,vehicle-车辆")
private String bizType;
/**
* 认证对象主键:司机ID 或 车辆ID(车辆按具体车辆,不是司机)
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "认证对象主键")
private Long bizId;
/**
* 展示键:司机姓名 / 车牌号,供审核列表直接展示
*/
@Schema(description = "展示键:司机姓名 / 车牌号")
private String bizKey;
/**
* 该次提交的全量认证内容(JSON),一经创建不可修改
* <p>
* {@code NEVER}:这是不变式 <b>I3</b> 的结构性保证——本字段<b>永不进入 UPDATE 语句</b>,
* 因此不存在「某条链路忘了保护快照」的可能。改内容只能靠<b>新提交一条记录</b>。
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@Schema(description = "该次提交的全量认证内容JSON")
private String snapshotJson;
/**
* 驳回原因,仅当审核状态为已驳回时有值
*/
@Schema(description = "驳回原因")
private String rejectReason;
/**
* 提交时间
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@Schema(description = "提交时间")
private LocalDateTime submitTime;
/**
* 提交人
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "提交人")
private Long submitUser;
/**
* 审核时间
*/
@Schema(description = "审核时间")
private LocalDateTime auditTime;
/**
* 审核人
*/
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "审核人")
private Long auditUser;
}
@@ -26,6 +26,9 @@
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -224,5 +227,21 @@ public class TransportVehicle extends TenantEntity {
/** 认证驳回原因 */
@Schema(description = "认证驳回原因")
private String certificationRejectReason;
/**
* 生效快照指针:指向 blade_transport_cert_record.id
* <p>
* 「该车辆是否已认证」的<b>唯一</b>判据。为 null 表示从未认证通过;
* 一经设置<b>永不回退</b>——重新提交、待审、被驳回都不得修改它(见 ADR-0002)。
* 改造后 {@link #certificationStatus} 降级为「最新提交状态」的展示缓存。
* <p>
* ⚠️ {@code READ_ONLY}:本字段<b>只能由审核链路</b>(03)写入。
* {@code POST /transport-vehicle/submit} 直接以本实体接收请求体,
* 若允许反序列化,调用方就能自行填一个记录 ID 把自己「认证」掉,
* 从而完全绕过审核与 D10 的可用性门槛。
*/
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "生效快照指针")
private Long effectiveCertRecordId;
}
@@ -0,0 +1,74 @@
/**
* 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.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.TransportCertRecord;
import java.io.Serial;
/**
* 认证记录视图实体类
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "认证记录")
public class TransportCertRecordVO extends TransportCertRecord {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
/**
* 提交人姓名
*/
@TableField(exist = false)
@Schema(description = "提交人姓名")
private String submitUserName;
/**
* 审核人姓名
*/
@TableField(exist = false)
@Schema(description = "审核人姓名")
private String auditUserName;
}