Merge remote-tracking branch 'websoft/dev'

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java
This commit is contained in:
2026-10-29 14:07:28 +08:00
93 changed files with 4723 additions and 542 deletions
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS `blade_loading_manage` (
`trailer_vehicle_no` varchar(100) DEFAULT NULL COMMENT '挂车车牌号',
`driver_name` varchar(100) DEFAULT NULL COMMENT '司机',
`driver_phone` varchar(50) DEFAULT NULL COMMENT '联系电话',
`driver_id` bigint(20) DEFAULT NULL COMMENT '司机ID',
`escort_name` varchar(100) DEFAULT NULL COMMENT '押运人',
`escort_phone` varchar(50) DEFAULT NULL COMMENT '押运人手机号',
`carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型',
@@ -50,7 +51,8 @@ CREATE TABLE IF NOT EXISTS `blade_loading_manage` (
UNIQUE KEY `uk_loading_manage_no` (`loading_no`) USING BTREE,
KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE,
KEY `idx_loading_manage_dept` (`dept_id`) USING BTREE,
KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE
KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE,
KEY `idx_loading_manage_tenant_driver_id` (`tenant_id`, `driver_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配载管理';
INSERT IGNORE INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES
@@ -0,0 +1,80 @@
-- ============================================================
-- blade_loading_manage_driver_id_20261028.sql
-- ============================================================
-- 运单/配载单写入司机档案 ID,把「归属」与「手机号快照」解耦:
-- blade_loading_manage.driver_id 配载单当前司机的司机档案 ID(blade_transport_driver.id)
-- blade_waybill.driver_id 已有列(本脚本只补索引),派单/配载/导入时写入
-- 归属判据:driver_id > 0 才算有主;0、-1 与 NULL 一律等同「无主」(承运商司机、未登录过小程序的司机)。
-- 历史数据不回填:手机号二次放号后按号反查会张冠李戴,旧单统一按无主处理,等下一次派单/配载再写入。
-- 本脚本只做结构升级,不改任何业务数据;以下结构变更可重复执行。
-- ------------------------------------------------------------
-- 0. 存档
-- ------------------------------------------------------------
-- 本脚本无数据订正,无需备份表。
-- ------------------------------------------------------------
-- 1. 结构变更
-- ------------------------------------------------------------
-- ① blade_loading_manage.driver_id
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_loading_manage` ADD COLUMN `driver_id` bigint(20) DEFAULT NULL COMMENT ''司机ID'' AFTER `driver_phone`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_loading_manage'
AND COLUMN_NAME = 'driver_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ② blade_loading_manage 按租户 + 司机归属检索(司机端/归属巡检)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_loading_manage` ADD KEY `idx_loading_manage_tenant_driver_id` (`tenant_id`, `driver_id`) USING BTREE',
'SELECT 1')
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_loading_manage'
AND INDEX_NAME = 'idx_loading_manage_tenant_driver_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ③ blade_waybill 按租户 + 司机归属检索(列已存在,只补索引)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_waybill` ADD KEY `idx_waybill_tenant_driver_id` (`tenant_id`, `driver_id`) USING BTREE',
'SELECT 1')
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_waybill'
AND INDEX_NAME = 'idx_waybill_tenant_driver_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ------------------------------------------------------------
-- 2. 校验
-- ------------------------------------------------------------
-- 列存在(期望 1 行)
-- SELECT COUNT(*) FROM information_schema.COLUMNS
-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_loading_manage' AND COLUMN_NAME = 'driver_id';
-- 索引存在(期望 2 行)
-- SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS
-- WHERE TABLE_SCHEMA = DATABASE()
-- AND INDEX_NAME IN ('idx_loading_manage_tenant_driver_id', 'idx_waybill_tenant_driver_id')
-- ORDER BY TABLE_NAME, SEQ_IN_INDEX;
-- 归属分布(历史单 driver_id 应为空)
-- SELECT COUNT(*) AS total, SUM(driver_id IS NOT NULL AND driver_id > 0) AS owned FROM blade_waybill;
-- ------------------------------------------------------------
-- 3. 回退
-- ------------------------------------------------------------
-- ALTER TABLE `blade_waybill` DROP INDEX `idx_waybill_tenant_driver_id`;
-- ALTER TABLE `blade_loading_manage` DROP INDEX `idx_loading_manage_tenant_driver_id`;
-- ALTER TABLE `blade_loading_manage` DROP COLUMN `driver_id`;
@@ -0,0 +1,239 @@
-- ============================================================
-- blade_loading_manage_driver_id_backfill_20261028.sql
-- ============================================================
-- 存量配载单补司机档案 ID:把「归属」从手机号快照切到 blade_transport_driver.id
-- 写业务数据。第 0 节建存档表,第 1 节分批回填,第 2 节校验(只读,执行后必看),第 3 节回退。可重复执行。
--
-- 为什么要订正:
-- 票 01 之后配载单的归属键是 driver_id(blade_transport_driver.id),driver_phone 只是配载时的号码快照。
-- 存量配载单全部只有号码、没有 driver_id(票 01 才加的列),司机端按档案 ID 查配载单会一张都查不到;
-- 按号码反查又会踩二次放号与改号,所以才需要先按「唯一命中」把能确定的补齐。
-- 本脚本只处理「号码在本租户内唯一命中一条未删除档案」的配载单,其余留在原样、进清单等人工处置。
--
-- 判定与边界(严格按票面口径,不扩大):
-- ① 只动 driver_id IS NULL 的行。driver_id = -1 是「无主」哨兵,正数表示已有归属,两者一行都不动;
-- 第 1 节的 WHERE 里另加了一道显式排除((driver_id = -1) IS NOT TRUE),
-- 防止后人把条件放宽成 driver_id <= 0 时把哨兵一起覆盖。
-- ② 唯一命中:同租户内、is_deleted = 0、mobile 与 driver_phone 相等的档案恰好 1 条。
-- 命中 0 条 → 进「无法匹配」清单;命中 ≥2 条 → 进「冲突」清单。两类都跳过,不猜。
-- 逻辑删除的档案不参与命中:档案删了以后重跑脚本,不能把这些单挂到废弃档案上。
-- ③ 手机号为空、空白、含 * 的跳过(* 是脱敏占位,不是真号码)。
-- ④ driver_phone、update_time 与其它任何列都不改,只写 driver_id。
-- ⑤ 逻辑删除的配载单同样回填:票面口径只有 driver_id IS NULL,不额外收窄范围;
-- 这些单不会回到列表,回填对它们无副作用。
-- ⑥ 跨表比较显式 COLLATE:blade_loading_manage.driver_phone / tenant_id 是 utf8mb4_0900_ai_ci,
-- blade_transport_driver.mobile / tenant_id 与存档表都是 utf8mb4_general_ci;
-- 不显式 COLLATE 会抛 ERROR 1267 Illegal mix of collations,只有真跑库才会暴露。
-- 第 2.4 节拿存档表比 driver_phone 同样要带 COLLATE utf8mb4_0900_ai_ci。
-- ⑦ 取数与写入全在 SQL 内完成,不经 JSON / 客户端中转:19 位雪花 ID 过 JSON 会丢精度。
-- ⑧ 分批执行(默认每批 500 行),每批独立提交,中断后重跑从剩余 NULL 行继续。
--
-- 范围说明:与运单脚本同一口径,只做「手机号 → 档案唯一命中」这一条规则;其余留给人工处置。
-- 幂等:命中映射先落存档表;重复执行时映射新增 0 行、回填更新 0 行,结果不变(第 2.2 节校验)。
-- 执行方式(脚本里有 DELIMITER 与存储过程,按分号切句的 GUI 客户端会切错):
-- mysql -h127.0.0.1 -uroot -p transport < blade_loading_manage_driver_id_backfill_20261028.sql
-- ------------------------------------------------------------
-- 0. 存档(必须在订正之前执行;重复执行不会覆盖已有存档)
-- ------------------------------------------------------------
-- 存档表同时是第 1 节的「命中映射」:一行 = 一个待回填的配载单 + 它唯一命中的档案 ID。
-- 回填前先把映射定下来:回填只按映射写,跑之前就能看出「打算填谁」,回退也按它还原。
CREATE TABLE IF NOT EXISTS blade_loading_manage_driver_id_bak_20261028 (
loading_id bigint NOT NULL COMMENT '配载单ID',
driver_id_before bigint DEFAULT NULL COMMENT '回填前的司机ID(本脚本只处理 NULL)',
driver_id_after bigint DEFAULT NULL COMMENT '回填写入的司机档案ID(blade_transport_driver.id)',
driver_phone varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '回填前的手机号快照,用来证明没有被改写',
update_time_before datetime DEFAULT NULL COMMENT '回填前的更新时间,用来证明没有被改写',
PRIMARY KEY (loading_id),
KEY idx_driver_id_after (driver_id_after)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='20261028 配载单司机档案ID回填存档';
-- 命中映射:只收「号码唯一命中一条未删除档案」的 NULL 行。
-- INSERT IGNORE + WHERE driver_id IS NULL:第二次执行不会重复建映射,已经填过的行不会再进映射。
INSERT IGNORE INTO blade_loading_manage_driver_id_bak_20261028 (loading_id, driver_id_before, driver_id_after, driver_phone, update_time_before)
SELECT matched.biz_id, NULL, matched.driver_id, matched.driver_phone, matched.update_time
FROM (
SELECT w.id AS biz_id,
MIN(d.id) AS driver_id,
w.driver_phone AS driver_phone,
w.update_time AS update_time
FROM blade_loading_manage w
JOIN blade_transport_driver d
ON d.tenant_id COLLATE utf8mb4_0900_ai_ci = w.tenant_id
AND d.mobile COLLATE utf8mb4_0900_ai_ci = w.driver_phone
AND d.is_deleted = 0
WHERE w.driver_id IS NULL
AND (w.driver_id = -1) IS NOT TRUE
AND w.driver_phone IS NOT NULL
AND TRIM(w.driver_phone) <> ''
AND w.driver_phone NOT LIKE '%*%'
GROUP BY w.id, w.driver_phone, w.update_time
HAVING COUNT(*) = 1
) matched;
-- ------------------------------------------------------------
-- 1. 订正(分批回填,默认每批 500 行)
-- ------------------------------------------------------------
-- 循环退出条件是「这一批没有更新任何行」,所以重复执行时第一批就是 0 行,脚本空转结束。
-- 每批单独提交(不开长事务):大表上不会长时间持锁,中断后重跑从剩余 NULL 行继续。
DELIMITER $$
DROP PROCEDURE IF EXISTS backfill_blade_loading_manage_driver_id_20261028$$
CREATE PROCEDURE backfill_blade_loading_manage_driver_id_20261028(IN p_batch_size INT)
BEGIN
DECLARE v_batch_size INT DEFAULT 500;
DECLARE v_affected INT DEFAULT 0;
IF p_batch_size IS NOT NULL AND p_batch_size > 0 THEN
SET v_batch_size = p_batch_size;
END IF;
CREATE TEMPORARY TABLE IF NOT EXISTS tmp_loading_manage_driver_id_20261028 (
loading_id bigint NOT NULL,
PRIMARY KEY (loading_id)
) ENGINE = InnoDB;
REPEAT
-- 取批:只取还空着的行,填过的行不再入批,所以每一批都在推进,不会原地打转
DELETE FROM tmp_loading_manage_driver_id_20261028;
INSERT INTO tmp_loading_manage_driver_id_20261028 (loading_id)
SELECT b.loading_id
FROM blade_loading_manage_driver_id_bak_20261028 b
JOIN blade_loading_manage w ON w.id = b.loading_id
WHERE w.driver_id IS NULL
ORDER BY b.loading_id
LIMIT v_batch_size;
UPDATE blade_loading_manage w
JOIN blade_loading_manage_driver_id_bak_20261028 b ON b.loading_id = w.id
JOIN tmp_loading_manage_driver_id_20261028 t ON t.loading_id = w.id
SET w.driver_id = b.driver_id_after
WHERE w.driver_id IS NULL
AND (w.driver_id = -1) IS NOT TRUE
AND b.driver_id_after IS NOT NULL;
SET v_affected = ROW_COUNT();
UNTIL v_affected = 0 END REPEAT;
DROP TEMPORARY TABLE IF EXISTS tmp_loading_manage_driver_id_20261028;
END$$
DELIMITER ;
CALL backfill_blade_loading_manage_driver_id_20261028(500);
DROP PROCEDURE IF EXISTS backfill_blade_loading_manage_driver_id_20261028;
-- ------------------------------------------------------------
-- 2. 校验(执行后必看;本节只有 SELECT,不动数据)
-- ------------------------------------------------------------
-- 2.1 规模:存档多少行、还剩多少 NULL 行、正数归属多少行(跑前跑后各查一次)
-- remaining_null_rows 只会减少「本次命中」的那部分;差额应当等于第 2.7 节的命中行数。
SELECT COUNT(*) AS archived_rows FROM blade_loading_manage_driver_id_bak_20261028;
SELECT COUNT(*) AS remaining_null_rows FROM blade_loading_manage WHERE driver_id IS NULL;
SELECT SUM(driver_id > 0) AS owned_rows,
SUM(driver_id = -1) AS sentinel_rows,
SUM(driver_id IS NULL) AS null_rows
FROM blade_loading_manage;
-- 2.2 哨兵不变:跑前跑后各执行一次,四列必须完全相同(-1 一行都不能动)
SELECT COUNT(*) AS sentinel_rows,
SUM(id) AS sentinel_id_sum,
MIN(id) AS min_id,
MAX(id) AS max_id
FROM blade_loading_manage
WHERE driver_id = -1;
-- 2.3 写入正确:本次映射的行,driver_id 必须等于映射值(期望 0 行)
SELECT COUNT(*) AS wrong_driver_id
FROM blade_loading_manage_driver_id_bak_20261028 b
JOIN blade_loading_manage w ON w.id = b.loading_id
WHERE NOT (w.driver_id <=> b.driver_id_after);
-- 2.4 快照没有被改写:本次映射的行,driver_phone 与 update_time 必须与存档逐行一致(期望 0 行)
SELECT COUNT(*) AS snapshot_rewritten
FROM blade_loading_manage_driver_id_bak_20261028 b
JOIN blade_loading_manage w ON w.id = b.loading_id
WHERE NOT (w.driver_phone <=> b.driver_phone COLLATE utf8mb4_0900_ai_ci)
OR NOT (w.update_time <=> b.update_time_before);
-- 2.5 冲突清单:同号多档案,这些行被跳过(期望 0 行;有输出就是「为什么没填」的答案)
SELECT w.driver_phone AS conflicting_phone,
COUNT(DISTINCT w.id) AS pending_rows,
COUNT(DISTINCT d.id) AS driver_profiles,
GROUP_CONCAT(DISTINCT d.id ORDER BY d.id) AS driver_profile_ids
FROM blade_loading_manage w
JOIN blade_transport_driver d
ON d.tenant_id COLLATE utf8mb4_0900_ai_ci = w.tenant_id
AND d.mobile COLLATE utf8mb4_0900_ai_ci = w.driver_phone
AND d.is_deleted = 0
WHERE w.driver_id IS NULL
AND (w.driver_id = -1) IS NOT TRUE
AND w.driver_phone IS NOT NULL
AND TRIM(w.driver_phone) <> ''
AND w.driver_phone NOT LIKE '%*%'
GROUP BY w.driver_phone
HAVING COUNT(DISTINCT d.id) > 1;
-- 2.6 无法匹配清单:号码在本租户内命中 0 条档案,这些行按规则跳过(等人工处置,脚本不猜)
SELECT w.driver_phone AS unmatched_phone,
COUNT(*) AS pending_rows
FROM blade_loading_manage w
WHERE w.driver_id IS NULL
AND (w.driver_id = -1) IS NOT TRUE
AND w.driver_phone IS NOT NULL
AND TRIM(w.driver_phone) <> ''
AND w.driver_phone NOT LIKE '%*%'
AND NOT EXISTS (
SELECT 1
FROM blade_transport_driver d
WHERE d.tenant_id COLLATE utf8mb4_0900_ai_ci = w.tenant_id
AND d.mobile COLLATE utf8mb4_0900_ai_ci = w.driver_phone
AND d.is_deleted = 0)
GROUP BY w.driver_phone
ORDER BY pending_rows DESC, unmatched_phone;
-- 2.7 逐司机对照:按手机号可见单量 vs 按档案 ID 可见单量
-- by_phone = 号码快照等于该档案号码的单量(旧口径,改号/二次放号会串)
-- by_driver_id = 档案 ID 等于该档案的单量(新口径)
-- phone_only_null = 号码对上、但归属还空着的单(本次应被回填的那部分)
-- phone_only_sentinel = 号码对上、但归属被标成 -1 无主的单(回填不碰,需要人工确认)
-- phone_only_other_owner = 号码对上、却已归属另一条档案的单(改号/换司机后号码被复用)
-- id_only_phone_differs = 已归属该档案、但号码快照不是该档案当前号码的单(改号后的历史快照)
SELECT d.id AS driver_id,
d.driver_name,
d.mobile,
(SELECT COUNT(*) FROM blade_loading_manage w WHERE w.driver_id = d.id) AS by_driver_id,
(SELECT COUNT(*) FROM blade_loading_manage w
WHERE w.tenant_id COLLATE utf8mb4_0900_ai_ci = d.tenant_id
AND w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile) AS by_phone,
(SELECT COUNT(*) FROM blade_loading_manage w
WHERE w.tenant_id COLLATE utf8mb4_0900_ai_ci = d.tenant_id
AND w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile
AND w.driver_id IS NULL) AS phone_only_null,
(SELECT COUNT(*) FROM blade_loading_manage w
WHERE w.tenant_id COLLATE utf8mb4_0900_ai_ci = d.tenant_id
AND w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile
AND w.driver_id = -1) AS phone_only_sentinel,
(SELECT COUNT(*) FROM blade_loading_manage w
WHERE w.tenant_id COLLATE utf8mb4_0900_ai_ci = d.tenant_id
AND w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile
AND w.driver_id > 0
AND w.driver_id <> d.id) AS phone_only_other_owner,
(SELECT COUNT(*) FROM blade_loading_manage w
WHERE w.driver_id = d.id
AND (w.driver_phone IS NULL
OR NOT (w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile))) AS id_only_phone_differs
FROM blade_transport_driver d
WHERE d.is_deleted = 0
ORDER BY by_phone DESC, by_driver_id DESC, driver_id;
-- ------------------------------------------------------------
-- 3. 回退(仅在需要撤销本次回填时手工执行)
-- ------------------------------------------------------------
-- 按存档表把本次写入的行还原成 NULL。只还原「当前值仍等于本次写入值」的行:
-- 回填之后又被人改派过的单不会被冲掉。
-- UPDATE blade_loading_manage w
-- JOIN blade_loading_manage_driver_id_bak_20261028 b ON b.loading_id = w.id
-- SET w.driver_id = b.driver_id_before
-- WHERE w.driver_id <=> b.driver_id_after;
-- 回退只碰 driver_id,不碰 driver_phone / update_time。备份表确认不再需要时再删:
-- DROP TABLE blade_loading_manage_driver_id_bak_20261028;
+5 -2
View File
@@ -300,7 +300,8 @@ CREATE TABLE `blade_waybill` (
KEY `idx_waybill_dept` (`dept_id`) USING BTREE,
KEY `idx_waybill_plan_id` (`plan_id`) USING BTREE,
KEY `idx_waybill_carrier_contract_id` (`carrier_contract_id`) USING BTREE,
KEY `idx_waybill_create_time` (`create_time`) USING BTREE
KEY `idx_waybill_create_time` (`create_time`) USING BTREE,
KEY `idx_waybill_tenant_driver_id` (`tenant_id`,`driver_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运单管理';
-- ----------------------------
@@ -330,6 +331,7 @@ CREATE TABLE `blade_loading_manage` (
`trailer_vehicle_no` varchar(100) DEFAULT NULL COMMENT '挂车车牌号',
`driver_name` varchar(100) DEFAULT NULL COMMENT '司机',
`driver_phone` varchar(50) DEFAULT NULL COMMENT '联系电话',
`driver_id` bigint(20) DEFAULT NULL COMMENT '司机ID',
`escort_name` varchar(100) DEFAULT NULL COMMENT '押运人',
`escort_phone` varchar(50) DEFAULT NULL COMMENT '押运人手机号',
`carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型',
@@ -359,7 +361,8 @@ CREATE TABLE `blade_loading_manage` (
UNIQUE KEY `uk_loading_manage_no` (`loading_no`) USING BTREE,
KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE,
KEY `idx_loading_manage_dept` (`dept_id`) USING BTREE,
KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE
KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE,
KEY `idx_loading_manage_tenant_driver_id` (`tenant_id`,`driver_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配载管理';
-- ----------------------------
@@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `blade_transport_cert_record` (
`biz_key` varchar(100) DEFAULT NULL COMMENT '展示键:司机姓名/车牌号',
`status` int(11) NOT NULL COMMENT '审核状态:0审核中 1已通过 2已驳回 3已作废',
`snapshot_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '该次提交的全量认证内容JSON,一经创建不可修改',
`audited_snapshot_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '审核通过时确定的修订版内容;NULL=未修订',
`reject_reason` varchar(200) DEFAULT NULL COMMENT '驳回原因,仅状态为已驳回时有值',
`submit_time` datetime DEFAULT NULL COMMENT '提交时间',
`submit_user` bigint(20) DEFAULT NULL COMMENT '提交人',
@@ -0,0 +1,68 @@
-- ----------------------------
-- 认证记录:新增修订版快照列 audited_snapshot_json(2026-10-28)
-- ----------------------------
-- 背景:审核员可以在审核页面对提交内容做修订(全字段可改,身份字段除外)。
-- 修订结果必须留痕:snapshot_json 是「司机当时提交了什么」,一经创建逐字节不可改;
-- 修订版另存一列,「审核通过时到底认定了什么内容」才查得清。
--
-- 语义:
-- NULL —— 未修订,生效内容即 snapshot_json;
-- 非 NULL —— 审核通过时认定的修订版全量 JSON,生效内容以它为准。
-- 只在「通过」时同事务写入;驳回不写(驳回不产生生效内容,留修订版只会让人误读)。
-- 修订人/修订时间复用 audit_user / audit_time,不新增列。
--
-- 只做结构变更,不回填数据:存量记录一律保持 NULL(历史审核确实没有修订动作)。
--
-- 不加索引:本列只在审核详情与生效内容读取时按主键取用,从不作为查询条件;
-- longtext 建索引会超出索引键长度并拖慢写入。
--
-- 幂等:先查 information_schema 判列,列已存在(或类型/可空性已符合)即跳过;
-- 只用 PREPARE 动态语句、不用存储过程,因此不需要 DELIMITER——按分号逐句切分的
-- 客户端(含数据库驱动)也能直接执行。重复执行零变更。
SET @cert_record_schema = DATABASE();
-- ----------------------------
-- 0. 表存在性守卫
-- ----------------------------
-- 表不存在时故意执行一条只读查询,让客户端报 1146「Table ... doesn't exist」;
-- 否则下面因查不到列而静默跳过,脚本会假绿。
SET @cert_record_guard_sql = (
SELECT IF(COUNT(*) > 0,
'SELECT 1',
'SELECT 1 FROM `blade_transport_cert_record` LIMIT 0')
FROM information_schema.tables
WHERE table_schema = @cert_record_schema
AND table_name = 'blade_transport_cert_record'
);
PREPARE cert_record_guard_stmt FROM @cert_record_guard_sql;
EXECUTE cert_record_guard_stmt;
DEALLOCATE PREPARE cert_record_guard_stmt;
-- ----------------------------
-- 1. 加列 audited_snapshot_json
-- ----------------------------
-- 位置紧接 snapshot_json:两列是同一份内容的前后两态,放一起便于对照。
-- 条件分三种:列不存在则 ADD;列存在但类型不是 longtext 或可空性不是 YES 则 MODIFY
-- (防止历史上的手工变更把列建成 varchar 而截断 JSON);其余情况跳过。
SET @cert_record_audited_snapshot_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_transport_cert_record` ADD COLUMN `audited_snapshot_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''审核通过时确定的修订版内容;NULL=未修订'' AFTER `snapshot_json`',
IF(SUM(data_type <> 'longtext' OR is_nullable <> 'YES') > 0,
'ALTER TABLE `blade_transport_cert_record` MODIFY COLUMN `audited_snapshot_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''审核通过时确定的修订版内容;NULL=未修订''',
'SELECT 1'))
FROM information_schema.columns
WHERE table_schema = @cert_record_schema
AND table_name = 'blade_transport_cert_record'
AND column_name = 'audited_snapshot_json'
);
PREPARE cert_record_audited_snapshot_stmt FROM @cert_record_audited_snapshot_sql;
EXECUTE cert_record_audited_snapshot_stmt;
DEALLOCATE PREPARE cert_record_audited_snapshot_stmt;
-- ----------------------------
-- 2. 回滚
-- ----------------------------
-- 只新增了一列、未进任何索引,回滚即删该列;
-- snapshot_json 的 updateStrategy=NEVER 与生成列 pending_biz_id、唯一键 uk_cert_record_pending 均不受影响:
-- ALTER TABLE `blade_transport_cert_record` DROP COLUMN `audited_snapshot_json`;
@@ -7,6 +7,7 @@ CREATE TABLE `blade_vehicle_maintenance_record` (
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000' COMMENT '租户ID',
`vehicle_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '车船类型',
`vehicle_no` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '车牌号/船号',
`exception_id` bigint NULL DEFAULT NULL COMMENT '关联异常处置ID(小程序录入时写入)',
`maintainer` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '维修人',
`maintenance_time` datetime NOT NULL COMMENT '维修时间',
`location` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '维修位置',
@@ -29,5 +30,6 @@ CREATE TABLE `blade_vehicle_maintenance_record` (
`is_deleted` int NULL DEFAULT 0 COMMENT '是否已删除',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_vehicle_maintenance_record_no`(`vehicle_no`) USING BTREE,
INDEX `idx_vehicle_maintenance_record_time`(`maintenance_time`) USING BTREE
INDEX `idx_vehicle_maintenance_record_time`(`maintenance_time`) USING BTREE,
UNIQUE INDEX `uk_vehicle_maintenance_record_exception`(`exception_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '车辆维修记录';
@@ -0,0 +1,35 @@
-- 维修记录关联异常处置
-- 日期:2026-10-29
-- 小程序调度端在「车辆故障维修」类异常下录入维修记录,记录通过 exception_id 关联该条异常。
-- 一条异常至多一条维修记录,故建唯一索引;唯一索引允许多个 NULL,PC 手工录入/导入的记录不受影响。
DELIMITER $$
DROP PROCEDURE IF EXISTS `upgrade_blade_vehicle_maintenance_record_exception_20261029`$$
CREATE PROCEDURE `upgrade_blade_vehicle_maintenance_record_exception_20261029`()
BEGIN
DECLARE db_name varchar(128);
SET db_name = DATABASE();
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = db_name
AND table_name = 'blade_vehicle_maintenance_record'
AND column_name = 'exception_id') THEN
ALTER TABLE `blade_vehicle_maintenance_record`
ADD COLUMN `exception_id` bigint NULL DEFAULT NULL COMMENT '关联异常处置ID(小程序录入时写入)'
AFTER `vehicle_no`;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics
WHERE table_schema = db_name
AND table_name = 'blade_vehicle_maintenance_record'
AND index_name = 'uk_vehicle_maintenance_record_exception') THEN
ALTER TABLE `blade_vehicle_maintenance_record`
ADD UNIQUE INDEX `uk_vehicle_maintenance_record_exception`(`exception_id`) USING BTREE;
END IF;
END$$
CALL `upgrade_blade_vehicle_maintenance_record_exception_20261029`()$$
DROP PROCEDURE `upgrade_blade_vehicle_maintenance_record_exception_20261029`$$
DELIMITER ;
@@ -0,0 +1,242 @@
-- ============================================================
-- blade_waybill_driver_id_backfill_20261028.sql
-- ============================================================
-- 存量运单补司机档案 ID:把「归属」从手机号快照切到 blade_transport_driver.id
-- 写业务数据。第 0 节建存档表,第 1 节分批回填,第 2 节校验(只读,执行后必看),第 3 节回退。可重复执行。
--
-- 为什么要订正:
-- 票 01 之后运单的归属键是 driver_id(blade_transport_driver.id),driver_phone 只是派单时的号码快照。
-- 存量运单大多只有号码、没有 driver_id,司机端按档案 ID 查单会整批漏掉它们;
-- 按号码反查又会踩二次放号与改号(本机库实测:黄宇富档案已改号 15577122721,
-- 名下 92 张单仍写着旧号 17587421208,其中 20 张 driver_id 为空、5 张能从接单司机推出来,
-- 剩下 15 张号码已经对不上任何档案——本脚本不猜,它们会出现在第 2.6 节清单里)。
-- 本脚本只处理「号码在本租户内唯一命中一条未删除档案」的运单,其余留在原样、进清单等人工处置。
--
-- 判定与边界(严格按票面口径,不扩大):
-- ① 只动 driver_id IS NULL 的行。driver_id = -1 是「无主」哨兵,正数表示已有归属,两者一行都不动;
-- 第 1 节的 WHERE 里另加了一道显式排除((driver_id = -1) IS NOT TRUE),
-- 防止后人把条件放宽成 driver_id <= 0 时把哨兵一起覆盖。
-- ② 唯一命中:同租户内、is_deleted = 0、mobile 与 driver_phone 相等的档案恰好 1 条。
-- 命中 0 条 → 进「无法匹配」清单;命中 ≥2 条 → 进「冲突」清单。两类都跳过,不猜。
-- 逻辑删除的档案不参与命中:档案删了以后重跑脚本,不能把这些单挂到废弃档案上。
-- ③ 手机号为空、空白、含 * 的跳过(* 是脱敏占位,不是真号码)。
-- ④ driver_phone、update_time 与其它任何列都不改,只写 driver_id。
-- ⑤ 逻辑删除的运单同样回填:票面口径只有 driver_id IS NULL,不额外收窄范围;
-- 这些单不会回到列表,回填对它们无副作用。
-- ⑥ 跨表比较显式 COLLATE:blade_waybill.driver_phone / tenant_id 是 utf8mb4_0900_ai_ci,
-- blade_transport_driver.mobile / tenant_id 与存档表都是 utf8mb4_general_ci;
-- 不显式 COLLATE 会抛 ERROR 1267 Illegal mix of collations,只有真跑库才会暴露。
-- 第 2.4 节拿存档表比 driver_phone 同样要带 COLLATE utf8mb4_0900_ai_ci。
-- ⑦ 取数与写入全在 SQL 内完成,不经 JSON / 客户端中转:19 位雪花 ID 过 JSON 会丢精度。
-- ⑧ 分批执行(默认每批 500 行),每批独立提交,中断后重跑从剩余 NULL 行继续。
--
-- 范围说明:本脚本只做「手机号 → 档案唯一命中」这一条规则。
-- 不引入 driver_accept_driver_id 推断,也不按姓名/历史号码猜归属:这些属于人工处置,在票面范围外。
-- 幂等:命中映射先落存档表;重复执行时映射新增 0 行、回填更新 0 行,结果不变(第 2.2 节校验)。
-- 执行方式(脚本里有 DELIMITER 与存储过程,按分号切句的 GUI 客户端会切错):
-- mysql -h127.0.0.1 -uroot -p transport < blade_waybill_driver_id_backfill_20261028.sql
-- ------------------------------------------------------------
-- 0. 存档(必须在订正之前执行;重复执行不会覆盖已有存档)
-- ------------------------------------------------------------
-- 存档表同时是第 1 节的「命中映射」:一行 = 一个待回填的运单 + 它唯一命中的档案 ID。
-- 回填前先把映射定下来:回填只按映射写,跑之前就能看出「打算填谁」,回退也按它还原。
CREATE TABLE IF NOT EXISTS blade_waybill_driver_id_bak_20261028 (
waybill_id bigint NOT NULL COMMENT '运单ID',
driver_id_before bigint DEFAULT NULL COMMENT '回填前的司机ID(本脚本只处理 NULL)',
driver_id_after bigint DEFAULT NULL COMMENT '回填写入的司机档案ID(blade_transport_driver.id)',
driver_phone varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '回填前的手机号快照,用来证明没有被改写',
update_time_before datetime DEFAULT NULL COMMENT '回填前的更新时间,用来证明没有被改写',
PRIMARY KEY (waybill_id),
KEY idx_driver_id_after (driver_id_after)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='20261028 运单司机档案ID回填存档';
-- 命中映射:只收「号码唯一命中一条未删除档案」的 NULL 行。
-- INSERT IGNORE + WHERE driver_id IS NULL:第二次执行不会重复建映射,已经填过的行不会再进映射。
INSERT IGNORE INTO blade_waybill_driver_id_bak_20261028 (waybill_id, driver_id_before, driver_id_after, driver_phone, update_time_before)
SELECT matched.biz_id, NULL, matched.driver_id, matched.driver_phone, matched.update_time
FROM (
SELECT w.id AS biz_id,
MIN(d.id) AS driver_id,
w.driver_phone AS driver_phone,
w.update_time AS update_time
FROM blade_waybill w
JOIN blade_transport_driver d
ON d.tenant_id COLLATE utf8mb4_0900_ai_ci = w.tenant_id
AND d.mobile COLLATE utf8mb4_0900_ai_ci = w.driver_phone
AND d.is_deleted = 0
WHERE w.driver_id IS NULL
AND (w.driver_id = -1) IS NOT TRUE
AND w.driver_phone IS NOT NULL
AND TRIM(w.driver_phone) <> ''
AND w.driver_phone NOT LIKE '%*%'
GROUP BY w.id, w.driver_phone, w.update_time
HAVING COUNT(*) = 1
) matched;
-- ------------------------------------------------------------
-- 1. 订正(分批回填,默认每批 500 行)
-- ------------------------------------------------------------
-- 循环退出条件是「这一批没有更新任何行」,所以重复执行时第一批就是 0 行,脚本空转结束。
-- 每批单独提交(不开长事务):大表上不会长时间持锁,中断后重跑从剩余 NULL 行继续。
DELIMITER $$
DROP PROCEDURE IF EXISTS backfill_blade_waybill_driver_id_20261028$$
CREATE PROCEDURE backfill_blade_waybill_driver_id_20261028(IN p_batch_size INT)
BEGIN
DECLARE v_batch_size INT DEFAULT 500;
DECLARE v_affected INT DEFAULT 0;
IF p_batch_size IS NOT NULL AND p_batch_size > 0 THEN
SET v_batch_size = p_batch_size;
END IF;
CREATE TEMPORARY TABLE IF NOT EXISTS tmp_waybill_driver_id_20261028 (
waybill_id bigint NOT NULL,
PRIMARY KEY (waybill_id)
) ENGINE = InnoDB;
REPEAT
-- 取批:只取还空着的行,填过的行不再入批,所以每一批都在推进,不会原地打转
DELETE FROM tmp_waybill_driver_id_20261028;
INSERT INTO tmp_waybill_driver_id_20261028 (waybill_id)
SELECT b.waybill_id
FROM blade_waybill_driver_id_bak_20261028 b
JOIN blade_waybill w ON w.id = b.waybill_id
WHERE w.driver_id IS NULL
ORDER BY b.waybill_id
LIMIT v_batch_size;
UPDATE blade_waybill w
JOIN blade_waybill_driver_id_bak_20261028 b ON b.waybill_id = w.id
JOIN tmp_waybill_driver_id_20261028 t ON t.waybill_id = w.id
SET w.driver_id = b.driver_id_after
WHERE w.driver_id IS NULL
AND (w.driver_id = -1) IS NOT TRUE
AND b.driver_id_after IS NOT NULL;
SET v_affected = ROW_COUNT();
UNTIL v_affected = 0 END REPEAT;
DROP TEMPORARY TABLE IF EXISTS tmp_waybill_driver_id_20261028;
END$$
DELIMITER ;
CALL backfill_blade_waybill_driver_id_20261028(500);
DROP PROCEDURE IF EXISTS backfill_blade_waybill_driver_id_20261028;
-- ------------------------------------------------------------
-- 2. 校验(执行后必看;本节只有 SELECT,不动数据)
-- ------------------------------------------------------------
-- 2.1 规模:存档多少行、还剩多少 NULL 行、正数归属多少行(跑前跑后各查一次)
-- remaining_null_rows 只会减少「本次命中」的那部分;差额应当等于第 2.7 节的命中行数。
SELECT COUNT(*) AS archived_rows FROM blade_waybill_driver_id_bak_20261028;
SELECT COUNT(*) AS remaining_null_rows FROM blade_waybill WHERE driver_id IS NULL;
SELECT SUM(driver_id > 0) AS owned_rows,
SUM(driver_id = -1) AS sentinel_rows,
SUM(driver_id IS NULL) AS null_rows
FROM blade_waybill;
-- 2.2 哨兵不变:跑前跑后各执行一次,四列必须完全相同(-1 一行都不能动)
SELECT COUNT(*) AS sentinel_rows,
SUM(id) AS sentinel_id_sum,
MIN(id) AS min_id,
MAX(id) AS max_id
FROM blade_waybill
WHERE driver_id = -1;
-- 2.3 写入正确:本次映射的行,driver_id 必须等于映射值(期望 0 行)
SELECT COUNT(*) AS wrong_driver_id
FROM blade_waybill_driver_id_bak_20261028 b
JOIN blade_waybill w ON w.id = b.waybill_id
WHERE NOT (w.driver_id <=> b.driver_id_after);
-- 2.4 快照没有被改写:本次映射的行,driver_phone 与 update_time 必须与存档逐行一致(期望 0 行)
SELECT COUNT(*) AS snapshot_rewritten
FROM blade_waybill_driver_id_bak_20261028 b
JOIN blade_waybill w ON w.id = b.waybill_id
WHERE NOT (w.driver_phone <=> b.driver_phone COLLATE utf8mb4_0900_ai_ci)
OR NOT (w.update_time <=> b.update_time_before);
-- 2.5 冲突清单:同号多档案,这些行被跳过(期望 0 行;有输出就是「为什么没填」的答案)
SELECT w.driver_phone AS conflicting_phone,
COUNT(DISTINCT w.id) AS pending_rows,
COUNT(DISTINCT d.id) AS driver_profiles,
GROUP_CONCAT(DISTINCT d.id ORDER BY d.id) AS driver_profile_ids
FROM blade_waybill w
JOIN blade_transport_driver d
ON d.tenant_id COLLATE utf8mb4_0900_ai_ci = w.tenant_id
AND d.mobile COLLATE utf8mb4_0900_ai_ci = w.driver_phone
AND d.is_deleted = 0
WHERE w.driver_id IS NULL
AND (w.driver_id = -1) IS NOT TRUE
AND w.driver_phone IS NOT NULL
AND TRIM(w.driver_phone) <> ''
AND w.driver_phone NOT LIKE '%*%'
GROUP BY w.driver_phone
HAVING COUNT(DISTINCT d.id) > 1;
-- 2.6 无法匹配清单:号码在本租户内命中 0 条档案,这些行按规则跳过(等人工处置,脚本不猜)
SELECT w.driver_phone AS unmatched_phone,
COUNT(*) AS pending_rows
FROM blade_waybill w
WHERE w.driver_id IS NULL
AND (w.driver_id = -1) IS NOT TRUE
AND w.driver_phone IS NOT NULL
AND TRIM(w.driver_phone) <> ''
AND w.driver_phone NOT LIKE '%*%'
AND NOT EXISTS (
SELECT 1
FROM blade_transport_driver d
WHERE d.tenant_id COLLATE utf8mb4_0900_ai_ci = w.tenant_id
AND d.mobile COLLATE utf8mb4_0900_ai_ci = w.driver_phone
AND d.is_deleted = 0)
GROUP BY w.driver_phone
ORDER BY pending_rows DESC, unmatched_phone;
-- 2.7 逐司机对照:按手机号可见单量 vs 按档案 ID 可见单量
-- by_phone = 号码快照等于该档案号码的单量(旧口径,改号/二次放号会串)
-- by_driver_id = 档案 ID 等于该档案的单量(新口径)
-- phone_only_null = 号码对上、但归属还空着的单(本次应被回填的那部分)
-- phone_only_sentinel = 号码对上、但归属被标成 -1 无主的单(回填不碰,需要人工确认)
-- phone_only_other_owner = 号码对上、却已归属另一条档案的单(改号/换司机后号码被复用)
-- id_only_phone_differs = 已归属该档案、但号码快照不是该档案当前号码的单(改号后的历史快照)
SELECT d.id AS driver_id,
d.driver_name,
d.mobile,
(SELECT COUNT(*) FROM blade_waybill w WHERE w.driver_id = d.id) AS by_driver_id,
(SELECT COUNT(*) FROM blade_waybill w
WHERE w.tenant_id COLLATE utf8mb4_0900_ai_ci = d.tenant_id
AND w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile) AS by_phone,
(SELECT COUNT(*) FROM blade_waybill w
WHERE w.tenant_id COLLATE utf8mb4_0900_ai_ci = d.tenant_id
AND w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile
AND w.driver_id IS NULL) AS phone_only_null,
(SELECT COUNT(*) FROM blade_waybill w
WHERE w.tenant_id COLLATE utf8mb4_0900_ai_ci = d.tenant_id
AND w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile
AND w.driver_id = -1) AS phone_only_sentinel,
(SELECT COUNT(*) FROM blade_waybill w
WHERE w.tenant_id COLLATE utf8mb4_0900_ai_ci = d.tenant_id
AND w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile
AND w.driver_id > 0
AND w.driver_id <> d.id) AS phone_only_other_owner,
(SELECT COUNT(*) FROM blade_waybill w
WHERE w.driver_id = d.id
AND (w.driver_phone IS NULL
OR NOT (w.driver_phone COLLATE utf8mb4_0900_ai_ci = d.mobile))) AS id_only_phone_differs
FROM blade_transport_driver d
WHERE d.is_deleted = 0
ORDER BY by_phone DESC, by_driver_id DESC, driver_id;
-- ------------------------------------------------------------
-- 3. 回退(仅在需要撤销本次回填时手工执行)
-- ------------------------------------------------------------
-- 按存档表把本次写入的行还原成 NULL。只还原「当前值仍等于本次写入值」的行:
-- 回填之后又被人改派过的单不会被冲掉。
-- UPDATE blade_waybill w
-- JOIN blade_waybill_driver_id_bak_20261028 b ON b.waybill_id = w.id
-- SET w.driver_id = b.driver_id_before
-- WHERE w.driver_id <=> b.driver_id_after;
-- 回退只碰 driver_id,不碰 driver_phone / update_time。备份表确认不再需要时再删:
-- DROP TABLE blade_waybill_driver_id_bak_20261028;
+3 -1
View File
@@ -5620,6 +5620,7 @@ CREATE TABLE `blade_vehicle_maintenance_record` (
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`vehicle_type` varchar(20) NOT NULL COMMENT '车船类型',
`vehicle_no` varchar(30) NOT NULL COMMENT '车牌号/船号',
`exception_id` bigint(20) DEFAULT NULL COMMENT '关联异常处置ID(小程序录入时写入)',
`maintainer` varchar(20) DEFAULT NULL COMMENT '维修人',
`maintenance_time` datetime NOT NULL COMMENT '维修时间',
`location` varchar(100) DEFAULT NULL COMMENT '维修位置',
@@ -5642,7 +5643,8 @@ CREATE TABLE `blade_vehicle_maintenance_record` (
`is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_vehicle_maintenance_record_no` (`vehicle_no`) USING BTREE,
KEY `idx_vehicle_maintenance_record_time` (`maintenance_time`) USING BTREE
KEY `idx_vehicle_maintenance_record_time` (`maintenance_time`) USING BTREE,
UNIQUE KEY `uk_vehicle_maintenance_record_exception` (`exception_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆维修记录';
-- ----------------------------