diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java index 80137624..d1c91951 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java @@ -255,11 +255,12 @@ public class Driver extends TenantEntity { * 生效快照指针:指向 blade_transport_cert_record.id *

* 「该司机是否已认证」的唯一判据。为 null 表示从未认证通过; - * 一经设置永不回退——提交、驳回、作废都不得修改它(见 ADR-0002)。 + * 一经设置永不回退——提交、驳回、作废都不得修改它, + * 因此重新提交认证不会让一个已认证的司机失去接单资格。 *

- * ⚠️ {@code READ_ONLY}:本字段只能由审核链路(03)写入。 + * ⚠️ {@code READ_ONLY}:本字段只能由审核通过时写入。 * 司机档案的 {@code /submit} 直接以本实体接收请求体,若允许反序列化, - * 调用方就能自行填一个记录 ID 把自己「认证」掉,绕过审核与 D10 的可用性门槛。 + * 调用方就能自行填一个记录 ID 把自己「认证」掉,绕过审核。 */ @JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonSerialize(using = ToStringSerializer.class) 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 93d0aab7..7fb2b419 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 @@ -46,16 +46,13 @@ import java.time.LocalDateTime; * 标记为「已作废」,而不是删除。判定某个对象「是否已认证」只看它持有的生效快照指针, * 与记录表里的任何一条记录的状态都无关。 *

- * 设计依据:{@code docs/adr/0001-certification-record-immutable-snapshot.md}, - * 术语见根工作区 {@code GLOSSARY.md}。 - *

* ⚠️ {@code status} 承载的是「审核状态」,不是 BladeX 的「启用 / 停用」。 * 该字段继承自 {@link TenantEntity},带来两条必须堵住的框架通道: * 一是 {@code BaseServiceImpl#resolveEntity} 在插入时会把 {@code null} 的 {@code status} * 静默填成 {@code 1}(恰好是 {@link #STATUS_APPROVED}); * 二是继承来的 {@code changeStatus(ids, 1)} 会直接写 {@code status},等于批量通过审核。 - * 二者都已在 {@code TransportCertRecordServiceImpl} 中封堵(前者校验抛错、后者覆写抛错), - * 审核一律走 03 的审核接口。 + * 二者都已在 {@code TransportCertRecordServiceImpl} 中封堵(前者校验抛错、后者覆写抛错)。 + * 审核必须逐条、带审核人与审核时间落库,不得借道 {@code changeStatus} 批量改状态。 * * @author Chill */ @@ -97,7 +94,7 @@ public class TransportCertRecord extends TenantEntity { /** * 认证对象类型:driver-司机,vehicle-车辆 *

- * {@code NEVER}:记录一经创建,认证对象的身份不可改(详见类注释的不变式 I3)。 + * {@code NEVER}:记录一经创建,认证对象的身份不可改。 */ @TableField(updateStrategy = FieldStrategy.NEVER) @Schema(description = "认证对象类型:driver-司机,vehicle-车辆") @@ -117,8 +114,8 @@ public class TransportCertRecord extends TenantEntity { /** * 该次提交的全量认证内容(JSON),一经创建不可修改 *

- * {@code NEVER}:这是不变式 I3 的结构性保证——本字段永不进入 UPDATE 语句, - * 因此不存在「某条链路忘了保护快照」的可能。改内容只能靠新提交一条记录。 + * {@code NEVER}:本字段永不进入 UPDATE 语句,因此不存在「某条链路 + * 忘了保护快照」的可能。改内容只能靠新提交一条记录。 */ @TableField(updateStrategy = FieldStrategy.NEVER) @Schema(description = "该次提交的全量认证内容JSON") diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java index d18a3008..62f091f8 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java @@ -231,13 +231,13 @@ public class TransportVehicle extends TenantEntity { * 生效快照指针:指向 blade_transport_cert_record.id *

* 「该车辆是否已认证」的唯一判据。为 null 表示从未认证通过; - * 一经设置永不回退——重新提交、待审、被驳回都不得修改它(见 ADR-0002)。 + * 一经设置永不回退——重新提交、待审、被驳回都不得修改它, + * 因此重新提交认证不会让一辆已认证的车掉出可用池。 * 改造后 {@link #certificationStatus} 降级为「最新提交状态」的展示缓存。 *

- * ⚠️ {@code READ_ONLY}:本字段只能由审核链路(03)写入。 + * ⚠️ {@code READ_ONLY}:本字段只能由审核通过时写入。 * {@code POST /transport-vehicle/submit} 直接以本实体接收请求体, - * 若允许反序列化,调用方就能自行填一个记录 ID 把自己「认证」掉, - * 从而完全绕过审核与 D10 的可用性门槛。 + * 若允许反序列化,调用方就能自行填一个记录 ID 把自己「认证」掉,绕过审核。 */ @JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonSerialize(using = ToStringSerializer.class) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportCertRecordService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportCertRecordService.java index 2d9559ad..9dda48a2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportCertRecordService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportCertRecordService.java @@ -34,8 +34,8 @@ import java.util.List; /** * 认证记录 服务类 *

- * 只承载认证记录本身的读写与数据模型不变式的守护; - * 提交(02/05)、审核(03)、可用性门槛(04)等业务链路在各自的票中实现。 + * 只承载认证记录本身的读写与数据模型约束的守护;提交、审核、可用性门槛等业务链路 + * 由各自的业务服务实现。 * * @author Chill */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportCertRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportCertRecordServiceImpl.java index eae06347..b64a8a15 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportCertRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportCertRecordServiceImpl.java @@ -89,8 +89,8 @@ public class TransportCertRecordServiceImpl extends BaseServiceImpl审核状态,不是「启用 / 停用」。 * 而 {@code BaseServiceImpl#changeStatus} 会绕过 {@link #validateRecord} 直接写 status, * 调用 {@code changeStatus(ids, 1)} 即等于批量通过审核—— - * 审核人、审核时间、驳回原因全部缺失,既破坏 I3 的留痕,也会凭空造出生效快照。 - * 审核必须走 03 的审核接口(作用于单条记录、带留痕)。 + * 审核人、审核时间、驳回原因全部缺失,既丢失审核留痕,也会凭空造出生效快照。 + * 审核必须逐条走带留痕的审核方法。 * * @param ids 主键集合 * @param status 状态 @@ -122,8 +122,7 @@ public class TransportCertRecordServiceImpl extends BaseServiceImpl * 排序用 {@code submit_time} 而非框架默认的 {@code create_time},是刻意为之: - * 「认证记录」的业务时间轴是提交,ticket 02 明确要求 - * 「查询某辆车的认证记录列表:按提交时间倒序」。 + * 认证记录的业务时间轴是提交时间,列表需按提交时间倒序展示每次提交。 * * @param bizType 认证对象类型 * @param bizId 认证对象主键 diff --git a/doc/sql/transport/blade_transport_cert_record_20261002.sql b/doc/sql/transport/blade_transport_cert_record_20261002.sql index b43c754d..09bba84d 100644 --- a/doc/sql/transport/blade_transport_cert_record_20261002.sql +++ b/doc/sql/transport/blade_transport_cert_record_20261002.sql @@ -1,33 +1,19 @@ --- 认证记录数据模型(司机 / 车辆共用一张不可变快照表) +-- 认证记录数据模型:新建 blade_transport_cert_record,为司机表、车辆表补认证字段与 +-- 生效快照指针,订正司机表排序规则,并新增认证审核菜单。 -- --- 背景与设计依据: --- docs/adr/0001-certification-record-immutable-snapshot.md(不可变快照 + 生效指针) --- docs/adr/0002-certification-does-not-block-usage.md(提交不影响可用性) --- 术语定义见根工作区 GLOSSARY.md +-- 认证对象(司机 / 车辆)每次提交产生一条不可变记录,审核只改记录的状态, +-- 不覆盖已提交的内容。司机与车辆共用本表,由 biz_type 区分。 -- --- 本脚本只做 schema 变更,不含任何业务数据订正: --- 1. 新建 blade_transport_cert_record(认证记录,司机与车辆共用,biz_type 区分) --- 2. blade_transport_driver 补认证字段、生效快照指针、身份证有效期 --- 3. blade_transport_vehicle 补生效快照指针 --- 4. blade_transport_driver 排序规则订正为 utf8mb4_general_ci --- 5. 新增「认证审核-司机 / 车辆」菜单与权限码 +-- ⚠️ effective_cert_record_id(生效快照指针)是判定「该对象是否已认证」的唯一依据: +-- NULL = 从未审核通过;一经写入永不回退,提交、驳回、作废都不得修改它。 +-- 因此「重新提交审核」不会让一辆已认证的车掉出可用池。 -- --- 幂等:全部语句可重复执行(建表用 IF NOT EXISTS,加列先查 information_schema)。 +-- 幂等:全部语句可重复执行(建表 IF NOT EXISTS、加列先查 information_schema)。 -- MySQL 5.7+ 兼容。 --- --- ⚠️ 生效快照指针(effective_cert_record_id)的语义: --- "该认证对象是否已认证"的**唯一**判据。为 NULL 表示从未通过审核。 --- 一旦被指向,**永不回退为 NULL**——提交、驳回、作废都不得修改它。 --- --- ⚠️ 留给后续票的两条约束(本脚本无法用 DDL 表达): --- 1) 指针所指记录的 status 必须恒为「已通过」(1),且不得再被改动。 --- 这正是不变式 I2 的后半句;由 03 的「已终态记录不可再次审核」保证。 --- 2) 指针所指记录的 biz_type / biz_id 必须与持有它的对象一致。 --- 由 03 写指针时保证(通过时用的是待审记录自身的 biz_id)。 --- --------------------------------------------------------------------------- +-- ---------------------------- -- 1. 认证记录表 --- --------------------------------------------------------------------------- +-- ---------------------------- CREATE TABLE IF NOT EXISTS `blade_transport_cert_record` ( `id` bigint(20) NOT NULL COMMENT '主键', `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID', @@ -55,9 +41,9 @@ CREATE TABLE IF NOT EXISTS `blade_transport_cert_record` ( KEY `idx_cert_record_submit_time` (`submit_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='认证记录'; --- --------------------------------------------------------------------------- +-- ---------------------------- -- 2~4. 加列与排序规则订正 --- --------------------------------------------------------------------------- +-- ---------------------------- DELIMITER $$ DROP PROCEDURE IF EXISTS `upgrade_transport_cert_record_20261002`$$ @@ -67,8 +53,8 @@ BEGIN SET db_name = DATABASE(); -- ---- 2. 司机表:认证字段 + 生效快照指针 ---- - -- certification_status 是「最新提交状态」的展示缓存,不是可用性判据; - -- 初始为 NULL 表示「从未提交」,与「审核中(0)」语义不同。 + -- certification_status 是「最新提交状态」的展示缓存;初始为 NULL 表示「从未提交」, + -- 与「审核中(0)」语义不同,故不可用 0 兜底。 IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_transport_driver' AND column_name = 'certification_status') THEN ALTER TABLE `blade_transport_driver` ADD COLUMN `certification_status` int(11) DEFAULT NULL COMMENT '最新提交状态展示缓存:0审核中 1已通过 2已驳回 3已作废,NULL为从未提交' AFTER `remark`; END IF; @@ -92,15 +78,16 @@ BEGIN -- ---- 3. 车辆表:生效快照指针 ---- -- 现有 certification_status / certification_reject_reason 保留,降级为「最新提交状态」展示缓存; - -- 可用性判定一律改读本指针(改造见 ticket 04)。 + -- 车辆可用性判定改读本指针。 IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_transport_vehicle' AND column_name = 'effective_cert_record_id') THEN ALTER TABLE `blade_transport_vehicle` ADD COLUMN `effective_cert_record_id` bigint(20) DEFAULT NULL COMMENT '生效快照指针:指向 blade_transport_cert_record.id,NULL 表示从未认证通过;一经设置永不回退' AFTER `certification_reject_reason`; END IF; -- ---- 4. 司机表排序规则订正 ---- - -- blade_transport_driver 建表未显式指定 COLLATE,MySQL 8+ 默认 utf8mb4_0900_ai_ci, - -- 与项目规范(utf8mb4_general_ci)不一致;blade_transport_vehicle 曾因此报 - -- Illegal mix of collations,故一并订正,避免司机认证 JOIN 重演同一故障。 + -- blade_transport_driver 建表未显式指定 COLLATE,MySQL 8+ 会落到服务器默认 + -- utf8mb4_0900_ai_ci,与项目规范(utf8mb4_general_ci)不一致。 + -- blade_transport_vehicle 曾因同类问题在 JOIN 时报 Illegal mix of collations, + -- 故在此一并订正,避免司机认证的关联查询重演同一故障。 IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = db_name AND table_name = 'blade_transport_driver' AND table_collation <> 'utf8mb4_general_ci') THEN ALTER TABLE `blade_transport_driver` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; END IF; @@ -108,7 +95,7 @@ BEGIN -- ---- 4.1 待审唯一键自修复 ---- -- 生成列必须同时排除「已逻辑删除」的行:MyBatis-Plus 的 removeById 只把 is_deleted 置 1, -- 不会改 status。若生成列只看 status = 0,一条被逻辑删除的待审记录会继续占用唯一键, - -- 导致该认证对象**永远无法再提交**(ticket 02 明确要求车辆认证按逻辑删除处理)。 + -- 导致该认证对象永远无法再次提交。 -- 建表语句已是正确版本;这里负责把按早期版本建过表的环境就地修正过来。 IF EXISTS ( SELECT 1 FROM information_schema.columns @@ -120,8 +107,9 @@ BEGIN END IF; -- ---- 4.2 tenant_id 收紧为 NOT NULL ---- - -- tenant_id 是待审唯一键 uk_cert_record_pending 的第一列,而 MySQL 的唯一索引**不对 NULL 去重**: - -- 若 tenant_id 允许为 NULL,两条 tenant_id IS NULL 的待审记录可以并存,I1 会被整个绕过。 + -- tenant_id 是待审唯一键 uk_cert_record_pending 的第一列,而 MySQL 的唯一索引不对 NULL 去重: + -- 若 tenant_id 允许为 NULL,两条 tenant_id IS NULL 的待审记录可以并存, + -- 「同一对象至多一条待审」的约束就被绕过了。 -- 本库三张表(driver / vehicle / waybill)实测均无 tenant_id 为 NULL 的行, -- 且「NOT NULL DEFAULT '000000'」是项目既有写法(doc/sql/transport 下 23 个脚本如此)。 IF EXISTS ( @@ -136,7 +124,7 @@ BEGIN -- ---- 4.3 车辆认证状态缓存词表对齐 ---- -- 改造前车辆侧是「0认证中 1认证通过 2认证驳回」,不含「已作废」; - -- 而 02/03 会把「被同对象再次提交取代」写成 3,届时该列无对应语义。 + -- 而新流程会把「被同对象再次提交取代」写成 3,届时该列无对应语义。 -- 取值 0/1/2 在两套词表下含义一致(进行中 / 通过 / 驳回),故这里只补 3 并统一措辞, -- 不改变任何既有数据的含义。仅改列注释(元数据),不动数据。 IF EXISTS ( @@ -162,12 +150,11 @@ DROP PROCEDURE `upgrade_transport_cert_record_20261002`$$ DELIMITER ; --- --------------------------------------------------------------------------- +-- ---------------------------- -- 5. 菜单与权限码 -- 挂在「运力管理」(2079000000000000001) 下,与司机管理 / 车辆管理同级。 --- 权限码按 spec 约定:列表用 cert_certification_audit_{driver|vehicle}, --- 审核动作用对应 _audit 后缀。 --- --------------------------------------------------------------------------- +-- 列表页权限码 cert_certification_audit_{driver|vehicle},审核动作加 _audit 后缀。 +-- ---------------------------- INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES (2079000000000000300, 2079000000000000001, 'cert_certification_audit_driver', '认证审核-司机', 'cert_certification_audit_driver', '/transportCapacity/cert-audit-driver', 'iconfont iconicon_doc', 4, 1, 0, 1, '', '司机认证待审列表', 0), (2079000000000000301, 2079000000000000300, 'cert_certification_audit_driver_audit', '审核', 'cert_certification_audit_driver_audit', '', '', 1, 2, 0, 1, NULL, '通过/驳回司机认证', 0),