diff --git a/doc/sql/transport/blade_transport_cert_backfill_20261007.sql b/doc/sql/transport/blade_transport_cert_backfill_20261007.sql new file mode 100644 index 00000000..91dd544e --- /dev/null +++ b/doc/sql/transport/blade_transport_cert_backfill_20261007.sql @@ -0,0 +1,277 @@ +-- ---------------------------- +-- 认证:存量司机 / 车辆补生效快照 +-- 写业务数据。执行前必须先建备份表(第 0 节),执行后必须跑第 3 节校验。可重复执行。 +-- ---------------------------- +-- +-- 为什么必须补: +-- 「能否接单」在运营状态之上叠加了认证门槛,判据是生效快照指针 +-- blade_transport_driver.effective_cert_record_id 与 +-- blade_transport_vehicle.effective_cert_record_id。这两列与门槛一起新增, +-- 新增时对存量对象一律为 NULL;门槛默认开启,于是存量司机与车辆全部被判成 +-- 「未认证」,接单被直接拒绝。它们在此之前一直正常承运,可用性是既成事实, +-- 不该因为换了判定方式就丢掉。 +-- +-- 处置口径:给尚无生效快照的存量对象补一条「已通过」记录,并把指针指过去。 +-- 这是自动认可、不是任何人的审核行为,故 audit_user / audit_time 一律留空, +-- 不凭空编造审核留痕。 +-- +-- 快照键名必须与 PersonCertSubmitRequest / VehicleCertSubmitRequest 完全一致。 +-- 键名对不上不会报错、只会静默丢值:司机端「重新认证」表单会缺字段, +-- 审核详情会把每个字段都报成「已变更」。改 DTO 字段名时务必同步本脚本。 +-- +-- 司机的从业资格证有效期起(qualificationValidStart)在快照里没有来源: +-- blade_transport_driver 没有对应列,管理端的同名录入项也是只收不存。 +-- 此处同样不造值,该键留空。 +-- +-- 司机快照里没有 mobile:审核列表的司机姓名/手机号取的是司机档案当前值, +-- 不是提交时的快照值,故无需写进快照。 +-- +-- 提交司机(submit_user)为什么一起补: +-- 审核列表的「司机姓名 / 司机手机号」取数路径是 submit_user -> 系统账号 -> 司机档案。 +-- submit_user 为空,这两列就永远空白,审核队列「找得到人、联系得上」的价值就没了。 +-- 车辆认证记录本身不含司机信息,其提交司机取「该车当前绑定的司机」—— +-- 车辆认证的提交链路强制校验只能操作本人绑定的车辆,故真实提交的车辆认证记录, +-- 其提交司机就是这辆车的绑定司机。一条车牌有多个绑定司机时 +-- (driving_vehicle 是逗号分隔的文本列、无唯一索引),取司机档案 update_time 最新者: +-- 库里没有绑定时间戳,这是可用的最好代理指标。 +-- 解析不到系统账号的(该司机根本没有 blade_user),留 NULL,不编造。 +-- +-- 号段:blade_transport_cert_record.id 由应用侧雪花算法发号(当前约 2.1e18), +-- 本脚本自行发号,必须落在应用永远到不了的区间,否则会与真实提交撞主键。 +-- 车辆与司机各占一段,段距 1e17,互不重叠,且都远低于 bigint 上限 9.22e18。 +-- 该号段同时是回退的识别依据(第 4 节)。 +-- ⚠️ 不要改成「取 MAX(id) + 偏移量」:雪花 id 每秒最多 +1000, +-- 取 MAX+1000000 的话应用约 17 分钟就能追平并开始撞号。 +-- ⚠️ 也不要把 id 读出来再拼 UPDATE 的 WHERE:id 约 2.1e18 超过 JS/JSON +-- 安全整数上限 2^53,传回数据库时末位被截断,UPDATE 会静默匹配 0 行—— +-- 记录插进去了、指针却没写,且不报任何错。故全程用 SQL 自关联 +-- (JOIN ... ON r.biz_id = v.id),id 只在库内流转。 +-- +-- 发号用会话变量而不是 ROW_NUMBER():窗口函数要 MySQL 8.0 起,本脚本按 5.7 也能执行写。 +-- +-- 前置条件:blade_transport_cert_record 表与两张业务表上的 +-- certification_status / certification_reject_reason / effective_cert_record_id +-- 列都已存在(见 blade_transport_cert_record_20261002.sql)。缺表时报 +-- 「Table ... doesn't exist」,先补跑那个脚本再执行本脚本。 + +-- ---------------------------- +-- 0. 备份(必须在订正之前执行,仅首次需要) +-- ---------------------------- +-- CREATE TABLE blade_transport_vehicle_certbak AS +-- SELECT id, certification_status, certification_reject_reason, effective_cert_record_id +-- FROM blade_transport_vehicle; +-- CREATE TABLE blade_transport_driver_certbak AS +-- SELECT id, certification_status, certification_reject_reason, effective_cert_record_id +-- FROM blade_transport_driver; +-- CREATE TABLE blade_transport_cert_record_bak AS SELECT * FROM blade_transport_cert_record; + +START TRANSACTION; + +-- ---------------------------- +-- 1. 为尚无生效快照的车辆补「已通过」记录 +-- ---------------------------- +-- 从段内已有的最大号之后接着发,而不是每次从段首发: +-- 订正过一次之后又导入了新车、再跑本脚本时,从段首发号会与已有记录撞主键。 +-- 变量存的是**段内序号**(段基址已减掉),下面 SELECT 里再加回段基址。 +SET @vehicle_seq := (SELECT IFNULL(MAX(id) - 9000000000000000000, 0) + FROM blade_transport_cert_record + WHERE id >= 9000000000000000000 AND id < 9100000000000000000); + +INSERT INTO blade_transport_cert_record + (id, tenant_id, biz_type, biz_id, biz_key, status, snapshot_json, + submit_time, submit_user, create_time, update_time, is_deleted) +SELECT 9000000000000000000 + (@vehicle_seq := @vehicle_seq + 1), + IFNULL(v.tenant_id, '000000'), 'vehicle', v.id, IFNULL(v.plate_no, ''), 1, + JSON_OBJECT( + 'plateNo', v.plate_no, + 'ownerName', v.owner_name, + 'plateColor', v.plate_color, + 'vehicleType', v.vehicle_type, + 'vin', v.vin, + 'outerLength', v.outer_length, + 'outerWidth', v.outer_width, + 'outerHeight', v.outer_height, + 'approvedLoadKg', v.approved_load_kg, + 'tractionMassKg', v.traction_mass_kg, + 'energyType', v.energy_type, + 'businessRelation', v.business_relation, + 'compulsoryScrapDate', v.compulsory_scrap_date, + 'compulsoryScrapLongTerm', IF(v.compulsory_scrap_long_term = 1, 1, 0), + 'drivingLicenseNo', v.driving_license_no, + 'drivingLicenseImage', v.driving_license_image, + 'drivingLicenseMainBack', v.driving_license_main_back, + 'drivingLicenseViceFront', v.driving_license_vice_front, + 'drivingLicenseViceBack', v.driving_license_vice_back, + 'drivingLicenseStartDate', v.driving_license_start_date, + 'drivingLicenseEndDate', v.driving_license_end_date, + 'drivingLicenseLongTerm', IF(v.driving_license_long_term = 1, 1, 0), + 'roadTransportCertNo', v.road_transport_cert_no, + 'roadTransportCertImage', v.road_transport_cert_image, + 'roadTransportCertStartDate', v.road_transport_cert_start_date, + 'roadTransportCertEndDate', v.road_transport_cert_end_date, + 'roadTransportCertLongTerm', IF(v.road_transport_cert_long_term = 1, 1, 0), + 'registrationNo', v.registration_no, + 'registrationImage', v.registration_image + ), + NOW(), + (SELECT dr.user_id + FROM blade_transport_driver dr + WHERE dr.is_deleted = 0 + AND dr.user_id IS NOT NULL + AND dr.driving_vehicle IS NOT NULL + AND FIND_IN_SET(v.plate_no, REPLACE(dr.driving_vehicle, ',', ',')) > 0 + ORDER BY dr.update_time DESC, dr.id DESC + LIMIT 1), + NOW(), NOW(), 0 +FROM blade_transport_vehicle v +LEFT JOIN blade_transport_cert_record r + ON r.biz_type = 'vehicle' AND r.biz_id = v.id + AND r.id >= 9000000000000000000 AND r.id < 9100000000000000000 +WHERE v.is_deleted = 0 + AND v.effective_cert_record_id IS NULL + AND r.id IS NULL +ORDER BY v.id; + +-- 回填指针:靠 biz_id 自关联,不把 id 带出数据库 +UPDATE blade_transport_vehicle v +JOIN blade_transport_cert_record r + ON r.biz_type = 'vehicle' AND r.biz_id = v.id + AND r.id >= 9000000000000000000 AND r.id < 9100000000000000000 AND r.status = 1 +SET v.effective_cert_record_id = r.id, + v.certification_status = 1, + v.certification_reject_reason = NULL +WHERE v.effective_cert_record_id IS NULL AND v.is_deleted = 0; + +-- ---------------------------- +-- 2. 为尚无生效快照的司机补「已通过」记录 +-- ---------------------------- +SET @driver_seq := (SELECT IFNULL(MAX(id) - 9100000000000000000, 0) + FROM blade_transport_cert_record + WHERE id >= 9100000000000000000 AND id < 9200000000000000000); + +INSERT INTO blade_transport_cert_record + (id, tenant_id, biz_type, biz_id, biz_key, status, snapshot_json, + submit_time, submit_user, create_time, update_time, is_deleted) +SELECT 9100000000000000000 + (@driver_seq := @driver_seq + 1), + IFNULL(d.tenant_id, '000000'), 'driver', d.id, IFNULL(d.driver_name, ''), 1, + JSON_OBJECT( + 'realName', d.driver_name, + 'idCardNo', d.id_card_no, + 'idCardFrontUrl', d.id_card_front, + 'idCardBackUrl', d.id_card_back, + 'idCardValidStart', d.id_card_valid_start, + 'idCardValidEnd', d.id_card_valid_end, + 'idCardLongTerm', IF(d.id_card_long_term = 1, 1, 0), + 'driverFileNo', d.driving_license_no, + 'driverClass', d.driving_type, + 'driverLicenseFrontUrl', d.driving_license_front, + 'driverLicenseBackUrl', d.driving_license_back, + 'driverValidStart', d.driving_license_start_date, + 'driverValidEnd', d.driving_license_end_date, + 'driverLongTerm', IF(d.driving_license_long_term = 1, 1, 0), + 'qualificationType', d.qualification_type, + 'qualificationNo', d.qualification_no, + 'qualificationFront', d.qualification_front, + 'qualificationBack', d.qualification_back, + 'qualificationValidEnd', d.qualification_end_date, + 'qualificationLongTerm', IF(d.qualification_long_term = 1, 1, 0), + 'headPhoto', d.head_photo + ), + NOW(), + -- 注册在司机档案上的用户 id 最直接;它为空时退回「按手机号找账号」 + -- (司机登录账号就是手机号)。两者都落空说明这名司机没有系统账号, + -- 此时留 NULL —— 审核列表的司机列本来就取不到人,编不出来。 + IFNULL(d.user_id, + (SELECT u.id + FROM blade_user u + WHERE u.account = d.mobile COLLATE utf8mb4_general_ci + ORDER BY u.id DESC + LIMIT 1)), + NOW(), NOW(), 0 +FROM blade_transport_driver d +LEFT JOIN blade_transport_cert_record r + ON r.biz_type = 'driver' AND r.biz_id = d.id + AND r.id >= 9100000000000000000 AND r.id < 9200000000000000000 +WHERE d.is_deleted = 0 + AND d.effective_cert_record_id IS NULL + AND r.id IS NULL +ORDER BY d.id; + +UPDATE blade_transport_driver d +JOIN blade_transport_cert_record r + ON r.biz_type = 'driver' AND r.biz_id = d.id + AND r.id >= 9100000000000000000 AND r.id < 9200000000000000000 AND r.status = 1 +SET d.effective_cert_record_id = r.id, + d.certification_status = 1, + d.certification_reject_reason = NULL +WHERE d.effective_cert_record_id IS NULL AND d.is_deleted = 0; + +COMMIT; + +-- ---------------------------- +-- 3. 校验(执行后必看,四个都必须返回 0) +-- ---------------------------- +-- 下列查询非 0 说明还有对象没有生效快照,门槛开着它们就不可用。 +-- SELECT COUNT(*) AS vehicle_missing FROM blade_transport_vehicle +-- WHERE effective_cert_record_id IS NULL AND is_deleted = 0; +-- SELECT COUNT(*) AS driver_missing FROM blade_transport_driver +-- WHERE effective_cert_record_id IS NULL AND is_deleted = 0; +-- +-- 下列查询非 0 说明指针指向的不是一条「已通过」记录,生效值取不到内容。 +-- SELECT COUNT(*) AS vehicle_bad_pointer +-- FROM blade_transport_vehicle v +-- LEFT JOIN blade_transport_cert_record r ON r.id = v.effective_cert_record_id +-- WHERE v.effective_cert_record_id IS NOT NULL AND (r.id IS NULL OR r.status <> 1); +-- SELECT COUNT(*) AS driver_bad_pointer +-- FROM blade_transport_driver d +-- LEFT JOIN blade_transport_cert_record r ON r.id = d.effective_cert_record_id +-- WHERE d.effective_cert_record_id IS NOT NULL AND (r.id IS NULL OR r.status <> 1); +-- +-- 快照字段完整性:确认关键字段没有因为键名写错而丢失。 +-- 司机快照必须含 driverClass(准驾车型),车辆快照必须含 drivingLicenseNo(行驶证档案编号)。 +-- SELECT COUNT(*) AS driver_snapshot_bad FROM blade_transport_cert_record +-- WHERE biz_type = 'driver' AND JSON_EXTRACT(snapshot_json, '$.driverClass') IS NULL; +-- SELECT COUNT(*) AS vehicle_snapshot_bad FROM blade_transport_cert_record +-- WHERE biz_type = 'vehicle' AND JSON_EXTRACT(snapshot_json, '$.drivingLicenseNo') IS NULL; +-- +-- 号段账目:号段内的记录数应等于本次订正的对象数(首次订正 = 车辆数 + 尚无快照的在用司机数), +-- 指针落在号段内的对象数也应等于它。两个数对不上说明有对象漏订正。 +-- SELECT COUNT(*) AS in_range_record FROM blade_transport_cert_record +-- WHERE id >= 9000000000000000000 AND id < 9200000000000000000; +-- SELECT COUNT(*) AS driver_backfilled FROM blade_transport_driver +-- WHERE effective_cert_record_id >= 9000000000000000000 +-- AND effective_cert_record_id < 9200000000000000000; +-- SELECT COUNT(*) AS vehicle_backfilled FROM blade_transport_vehicle +-- WHERE effective_cert_record_id >= 9000000000000000000 +-- AND effective_cert_record_id < 9200000000000000000; + +-- ---------------------------- +-- 4. 回退 +-- ---------------------------- +-- 识别依据是 id 号段(>= 9.0e18),不是「备份表里没有的 id」: +-- 号段法不依赖备份表是否建过,也不会误删订正之后产生的真实提交记录。 +-- 回退前先把门槛开关置 false(transport.certification.driver-gate-enabled / +-- vehicle-gate-enabled),否则清掉指针的瞬间所有对象都不可用。 +-- 顺序:先清指针,再删记录;反了会在中间态留下悬空指针。 +-- START TRANSACTION; +-- UPDATE blade_transport_driver +-- SET effective_cert_record_id = NULL, certification_status = NULL, +-- certification_reject_reason = NULL +-- WHERE effective_cert_record_id >= 9000000000000000000 +-- AND effective_cert_record_id < 9200000000000000000; +-- UPDATE blade_transport_vehicle +-- SET effective_cert_record_id = NULL, certification_status = 0, +-- certification_reject_reason = NULL +-- WHERE effective_cert_record_id >= 9000000000000000000 +-- AND effective_cert_record_id < 9200000000000000000; +-- DELETE FROM blade_transport_cert_record +-- WHERE id >= 9000000000000000000 AND id < 9200000000000000000; +-- COMMIT; +-- +-- 车辆 certification_status 还原为 0 是近似值:订正会把原本 0 与 1 的车都写成 1, +-- 原分布无法反推。这只影响页面上的状态标,不影响可用性(重跑订正会再次全部写成 1)。 +-- 要精确还原只能用订正前建好的 blade_transport_vehicle_certbak: +-- UPDATE blade_transport_vehicle v +-- JOIN blade_transport_vehicle_certbak b ON b.id = v.id +-- SET v.certification_status = b.certification_status, +-- v.certification_reject_reason = b.certification_reject_reason;