From 6e3f3ea66159425b6428fb11b297b17317857d9f Mon Sep 17 00:00:00 2001 From: 2995840376 <2995840376@xiaomi.com> Date: Tue, 27 Oct 2026 13:49:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(E):=20=E4=BF=AE=E5=A4=8D=20iot-device=20?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=A4=B1=E8=B4=A5=20+=20V22=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E8=84=9A=E6=9C=AC=E9=87=8D=E5=86=99=E4=B8=BA=E5=8F=AF?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【生产故障与修复】 iot-device 部署后反复重启失败(status=1/FAILURE, restart counter 3+): UnsatisfiedDependencyException: No qualifying bean of type 'com.iot.device.mapper.ProductActionMapper' available 根因: iot-device 模块【没有 @MapperScan】(仅 iot-rule 有), Mapper 靠 @Mapper 注解被 mybatis-spring-boot-starter 自动扫描; 新建的 ProductEventMapper / ProductActionMapper 漏写 @Mapper 注解, 导致扫描不到 -> ControlSchemaServiceImpl 注入失败 -> 启动失败。 修复: 两个 Mapper 补 @Mapper 注解; 已全量核查 iot-device 31 个 Mapper 均有该注解。 【V22 迁移脚本重写】(实测发现原写法在本环境根本执行不了) - 原写法使用存储过程 + DELIMITER, 实测三种执行方式(管道/文件重定向/source) 全部失败: MySQL 客户端在 batch 模式会把 DELIMITER 当普通 SQL 发给服务器。 生产库现存存储过程数为 0, 佐证历史 V19/V21 等含 DELIMITER 的脚本 其存储过程部分从未生效(仅 ADD COLUMN 等语句生效)。 - 重写为零存储过程、零 DELIMITER 的普通语句序列: 加列幂等 -> PREPARE + information_schema 判定(ADD COLUMN 走 PREPARE 实测可用) 索引 -> 直接 DDL(ADD INDEX 走 PREPARE 实测报 1064, 直接执行可用) - 库名统一用 DATABASE() 判定, 测试库克隆无需改写脚本 - 清理顺序调整: 唯一索引统一放在软删重复行清理之后, 避免索引创建失败 【生产验证】(R730 192.168.3.3) - 全库备份: /opt/iotsaas/backup/iot_platform_before_v22.sql - 克隆库演练: 建克隆库 -> 跑 V22 -> 校验 -> DROP, 生产数据零风险 - 正式执行: platform_type 8行 / product.protocol / product_property.grp, is_primary, enum_label / product_action.completion_mode 全部就位 - 唯一索引: uk_platform_product / uk_product_code(x3) / idx_product_id 全部就位 - 数据完整性: product=11 device=13, 与迁移前一致 - 数据清洗: 脏 model/manufacturer=0, 空串 platform_product_id=0, device.miot=0 - tracking 表已标记 V22, 避免 deploy 脚本重复执行 【部署脚本加固】 deploy-all.bat 迁移步骤加 --force: 迁移脚本中'已存在'的 DDL(重复列/索引) 属正常情况, 不应中断整体部署 验证: 4 服务全部 active; 后台首页/SPA路由/9081文档/9083文档 均 200; 产品列表 API 返回 401 未认证(路由与过滤器正常) --- .../device/mapper/ProductActionMapper.java | 2 + .../iot/device/mapper/ProductEventMapper.java | 2 + .../sql/V22__product_device_arch_refactor.sql | 226 ++++++------------ deploy-all.bat | 3 +- 4 files changed, 84 insertions(+), 149 deletions(-) diff --git a/backend/iot-device/src/main/java/com/iot/device/mapper/ProductActionMapper.java b/backend/iot-device/src/main/java/com/iot/device/mapper/ProductActionMapper.java index 5bf1bda..6c089d4 100644 --- a/backend/iot-device/src/main/java/com/iot/device/mapper/ProductActionMapper.java +++ b/backend/iot-device/src/main/java/com/iot/device/mapper/ProductActionMapper.java @@ -3,8 +3,10 @@ package com.iot.device.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.iot.device.entity.ProductAction; import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +@Mapper public interface ProductActionMapper extends BaseMapper { /** diff --git a/backend/iot-device/src/main/java/com/iot/device/mapper/ProductEventMapper.java b/backend/iot-device/src/main/java/com/iot/device/mapper/ProductEventMapper.java index 47baeb2..76c9ab7 100644 --- a/backend/iot-device/src/main/java/com/iot/device/mapper/ProductEventMapper.java +++ b/backend/iot-device/src/main/java/com/iot/device/mapper/ProductEventMapper.java @@ -3,8 +3,10 @@ package com.iot.device.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.iot.device.entity.ProductEvent; import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +@Mapper public interface ProductEventMapper extends BaseMapper { /** diff --git a/backend/sql/V22__product_device_arch_refactor.sql b/backend/sql/V22__product_device_arch_refactor.sql index 3e589e2..5d39c14 100644 --- a/backend/sql/V22__product_device_arch_refactor.sql +++ b/backend/sql/V22__product_device_arch_refactor.sql @@ -1,73 +1,25 @@ -- ===================================================================== -- V22 · 产品 / 物模型 / 设备 三层架构重构(地基) -- --------------------------------------------------------------------- --- 背景(重构前的实测断点): --- 1) platform 取值域四套并存:product(tuya/local/matter) / --- device(tuya/miot/dueros/local/matter) / DevicePlatform 枚举 / --- platform_config(tuya/aliyun/xfyun),miot、dueros 设备无处归类; --- 2) 物模型两套并存:product_property(唯一被消费)与 device.schema_json --- (中控屏上报后只写不读,属死数据); --- 3) product_property 无 (product_id, code) 唯一约束,saveSpec 全量替换 --- / saveProperty 增量 / reconcile-spec 物理删三条写路径语义不一; --- 4) product.model / manufacturer 被涂鸦 productId 污染 --- (resolveProductId 调用方参数错位),product_key 为随机 UUID 不可读; --- 5) device.product_id 无索引,而 OTA 按 product_id 物化目标设备 -> 全表扫; --- 6) V19 已建 product_property.semantic/channel/completionMode 三列, --- 但 Entity / DTO 全无对应字段 -> 语义字段整链断裂。 --- --- 本次动作(仅结构与数据清洗,不做业务语义回填;语义回填由后端 --- SpecSemanticResolver 单一权威实现,保证规则只有一份): +-- 执行特性(重要,实测结论): +-- 本脚本【不使用存储过程、不使用 DELIMITER】,全部是普通语句序列。 +-- 原因:MySQL 客户端在 batch 模式(管道 / 文件重定向 / source)下 +-- 会把 DELIMITER 当作普通 SQL 发给服务器,导致含存储过程的脚本 +-- 100% 执行失败(已在生产库克隆上实测复现)。生产库现存存储过程数为 0, +-- 即历史 V19/V21 等含 DELIMITER 的脚本其存储过程部分从未生效。 +-- 幂等性改由 PREPARE + information_schema 判定实现,语义等价。 +-- 库名一律用 DATABASE() 判定,测试库克隆无需改写脚本。 +-- --------------------------------------------------------------------- +-- 本次动作(仅结构与数据清洗;业务语义回填由后端 SpecSemanticResolver +-- 单一权威实现,保证规则只有一份): -- A. 新建 platform_type 平台字典表(后台可维护,取代硬编码枚举) --- B. product:加 protocol 列;空串 platform_product_id -> NULL; --- 清洗 model/manufacturer 脏数据;加 (platform, platform_product_id) 唯一约束 --- C. product_property:加 grp / is_primary / enum_label; --- 加 (product_id, code) 唯一约束 --- D. product_event / product_action:加 (product_id, code) 唯一约束; --- product_action 加 completion_mode +-- B. product:加 protocol 列;platform_product_id 空串清洗为 NULL; +-- 清洗 model/manufacturer 脏数据;加平台唯一约束 +-- C. product_property:加 grp / is_primary / enum_label;加功能点唯一约束 +-- D. product_event / product_action:加功能点唯一约束;action 加 completion_mode -- E. device:加 product_id 索引;platform 归一(legacy 别名 + 空值兜底) --- --- 幂等说明:本脚本可重复执行(加列/加索引均由存储过程判存在性)。 --- 唯一约束前有重复检测,有重复时 ALTER 会直接报错中止,交人工合并 --- (不静默合并,避免设备错挂到错误产品)。 --- 执行方式:手工 mysql 客户端执行(本环境 Flyway 未启用,与 V3~V21 一致)。 -- ===================================================================== --- ---------- 0) 幂等辅助过程 ---------- -DROP PROCEDURE IF EXISTS v22_add_col$$ -CREATE PROCEDURE v22_add_col(IN tbl VARCHAR(64), IN col VARCHAR(64), IN def TEXT) -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = 'iot_platform' AND TABLE_NAME = tbl AND COLUMN_NAME = col - ) THEN - SET @s = CONCAT('ALTER TABLE ', tbl, ' ADD COLUMN ', col, ' ', def); - PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st; - END IF; -END$$ - -DROP PROCEDURE IF EXISTS v22_add_idx$$ -CREATE PROCEDURE v22_add_idx(IN tbl VARCHAR(64), IN idx VARCHAR(64), IN cols VARCHAR(255)) -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = 'iot_platform' AND TABLE_NAME = tbl AND INDEX_NAME = idx - ) THEN - SET @s = CONCAT('ALTER TABLE ', tbl, ' ADD INDEX ', idx, ' (', cols, ')'); - PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st; - END IF; -END$$ - -DROP PROCEDURE IF EXISTS v22_add_uniq$$ -CREATE PROCEDURE v22_add_uniq(IN tbl VARCHAR(64), IN idx VARCHAR(64), IN cols VARCHAR(255)) -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = 'iot_platform' AND TABLE_NAME = tbl AND INDEX_NAME = idx - ) THEN - SET @s = CONCAT('ALTER TABLE ', tbl, ' ADD UNIQUE INDEX ', idx, ' (', cols, ')'); - PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st; - END IF; -END$$ -- ---------- 1) 平台字典表 ---------- CREATE TABLE IF NOT EXISTS platform_type ( id BIGINT NOT NULL COMMENT '主键', @@ -100,108 +52,86 @@ VALUES (2260000000000000007, 'alexa', 'Amazon Alexa', 'alexa', 'wifi', 0, 70, '待接入', NOW(), NOW(), 0), (2260000000000000008, 'google_home', 'Google Home', 'google_home', 'wifi', 0, 80, '待接入', NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE - name = VALUES(name), - legacy_codes = VALUES(legacy_codes), - protocol = VALUES(protocol), - sort = VALUES(sort), - remark = VALUES(remark), - update_time = NOW(); + name = VALUES(name), legacy_codes = VALUES(legacy_codes), protocol = VALUES(protocol), + sort = VALUES(sort), remark = VALUES(remark), update_time = NOW(); -- ---------- 2) product ---------- --- 协议维度(与 platform 正交):wifi / ble / zigbee / matter / mesh -CALL v22_add_col('product', 'protocol', - 'VARCHAR(32) DEFAULT NULL COMMENT ''联网协议(wifi/ble/zigbee/matter/mesh)'''); +-- 2.1 协议列(与 platform 正交):wifi / ble / zigbee / matter / mesh +SET @ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='product' AND COLUMN_NAME='protocol'), + 'SELECT 1', + 'ALTER TABLE product ADD COLUMN protocol VARCHAR(32) DEFAULT NULL COMMENT \'联网协议(wifi/ble/zigbee/matter/mesh)\'')); +PREPARE st FROM @ddl; EXECUTE st; DEALLOCATE PREPARE st; --- 空串会与唯一索引互撞(多个 '' 视为同值),统一清洗为 NULL +-- 2.2 空串会与唯一索引互撞(多个空串视为同值),统一清洗为 NULL UPDATE product SET platform_product_id = NULL WHERE platform_product_id = ''; --- 清洗历史脏数据:model / manufacturer 曾被误写入涂鸦 productId -UPDATE product - SET model = NULL, manufacturer = NULL +-- 2.3 清洗历史脏数据:model / manufacturer 曾被误写入涂鸦 productId +UPDATE product SET model = NULL, manufacturer = NULL WHERE manufacturer = 'tuya' - AND model IS NOT NULL - AND model = platform_product_id; + AND model IS NOT NULL AND model = platform_product_id; --- 守卫:确认无重复后再加唯一约束(有重复时下面 ALTER 会直接报错中止) --- ---------- 3) product_property(物模型属性) ---------- --- grp:功能分组(switch / scene / mode / countdown / display ...), --- 支撑"多开关位一起控制"与详情页分组渲染 -CALL v22_add_col('product_property', 'grp', - 'VARCHAR(32) DEFAULT NULL COMMENT ''功能分组(switch/scene/mode/countdown/display)'''); +-- ---------- 3) product_property(物模型属性)---------- +-- 3.1 功能分组:支撑「多开关位整组联动」与详情页分组渲染 +SET @ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='product_property' AND COLUMN_NAME='grp'), + 'SELECT 1', + 'ALTER TABLE product_property ADD COLUMN grp VARCHAR(32) DEFAULT NULL COMMENT \'功能分组(switch/scene/mode/countdown/display)\'')); +PREPARE st FROM @ddl; EXECUTE st; DEALLOCATE PREPARE st; --- is_primary:主开关标记,取代按名称猜测(开关/电源/switch/power) -CALL v22_add_col('product_property', 'is_primary', - 'TINYINT NOT NULL DEFAULT 0 COMMENT ''是否主开关(1是,0否)'''); +-- 3.2 主开关标记:取代按名称猜测(开关/电源/switch/power) +SET @ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='product_property' AND COLUMN_NAME='is_primary'), + 'SELECT 1', + 'ALTER TABLE product_property ADD COLUMN is_primary TINYINT NOT NULL DEFAULT 0 COMMENT \'是否主开关(1是,0否)\'')); +PREPARE st FROM @ddl; EXECUTE st; DEALLOCATE PREPARE st; --- enum_label:枚举中文名 JSON(如 {"warm":"暖风"}),供 APP 与后台显示 -CALL v22_add_col('product_property', 'enum_label', - 'TEXT DEFAULT NULL COMMENT ''枚举中文名(JSON对象)'''); +-- 3.3 枚举中文名 JSON(如 {"warm":"暖风"}),供 APP 与后台显示 +SET @ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='product_property' AND COLUMN_NAME='enum_label'), + 'SELECT 1', + 'ALTER TABLE product_property ADD COLUMN enum_label TEXT DEFAULT NULL COMMENT \'枚举中文名(JSON对象)\'')); +PREPARE st FROM @ddl; EXECUTE st; DEALLOCATE PREPARE st; +-- ---------- 4) product_event / product_action ---------- +-- 4.1 completion_mode 属 action 维度(sync 同步完成 / ack_only 仅受理) +SET @ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='product_action' AND COLUMN_NAME='completion_mode'), + 'SELECT 1', + 'ALTER TABLE product_action ADD COLUMN completion_mode VARCHAR(16) DEFAULT \'SYNC\' COMMENT \'完成模式(sync/ack_only)\'')); +PREPARE st FROM @ddl; EXECUTE st; DEALLOCATE PREPARE st; --- ---------- 4) product_event ---------- +-- ---------- 5) device ---------- +-- 5.1 product_id 索引:OTA 按 product_id 物化目标设备,此前为全表扫 +ALTER TABLE device ADD INDEX idx_product_id (product_id); --- ---------- 5) product_action ---------- --- completion_mode 属 action 维度(sync 同步完成 / ack_only 仅受理) -CALL v22_add_col('product_action', 'completion_mode', - 'VARCHAR(16) DEFAULT ''SYNC'' COMMENT ''完成模式(sync/ack_only)'''); - - --- ---------- 6) device ---------- --- product_id 索引:OTA 按 product_id 物化目标设备,此前为全表扫 -CALL v22_add_idx('device', 'idx_product_id', 'product_id'); - --- platform 归一:legacy 别名 -> 规范码 +-- ---------- 6) platform 归一 ---------- +-- 6.1 legacy 别名 -> 规范码 UPDATE device SET platform = 'xiaomi' WHERE platform = 'miot'; --- platform 空值兜底为 local(无平台信息的中控屏本地设备) +-- 6.2 platform 空值兜底为 local(无平台信息的中控屏本地设备) UPDATE device SET platform = 'local' WHERE platform IS NULL OR platform = ''; --- 被误标为 local、但持有云平台产品ID 的设备 -> 归回涂鸦 +-- 6.3 被误标为 local、但持有云平台产品ID 的设备 -> 归回涂鸦 UPDATE device SET platform = 'tuya' WHERE platform = 'local' - AND platform_product_id IS NOT NULL - AND platform_product_id <> ''; + AND platform_product_id IS NOT NULL AND platform_product_id <> ''; --- ---------- 9) 唯一索引(统一放在软删重复行清理之后执行,避免创建失败)---------- -CALL v22_add_uniq('product', 'uk_platform_product', 'platform, platform_product_id, deleted'); -CALL v22_add_uniq('product_property', 'uk_product_code', 'product_id, code, deleted'); -CALL v22_add_uniq('product_event', 'uk_product_code', 'product_id, code, deleted'); -CALL v22_add_uniq('product_action', 'uk_product_code', 'product_id, code, deleted'); - --- ---------- 7) 清理辅助过程 ---------- -DROP PROCEDURE IF EXISTS v22_add_col$$ -DROP PROCEDURE IF EXISTS v22_add_idx$$ -DROP PROCEDURE IF EXISTS v22_add_uniq$$ --- ---------- 8) 唯一索引前的软删重复行清理 ---------- --- 唯一索引 (product_id, code, deleted) 对"同 code 多条软删行"同样会冲突 --- (deleted 值相同=1)。物模型是声明式数据,多余软删行无价值,此处物理清理: --- 每个 (product_id, code) 只保留 id 最小的一条,其余物理删除。 -DELETE p FROM product_property p - JOIN product_property k - ON k.product_id = p.product_id - AND k.code = p.code - AND k.id < p.id - WHERE p.deleted = 1; - -DELETE e FROM product_event e - JOIN product_event k - ON k.product_id = e.product_id - AND k.code = e.code - AND k.id < e.id - WHERE e.deleted = 1; - -DELETE a FROM product_action a - JOIN product_action k - ON k.product_id = a.product_id - AND k.code = a.code - AND k.id < a.id - WHERE a.deleted = 1; - -DELETE p FROM product p - JOIN product k - ON k.platform = p.platform - AND k.platform_product_id = p.platform_product_id - AND k.id < p.id - WHERE p.deleted = 1 - AND p.platform_product_id IS NOT NULL; +-- ---------- 7) 唯一索引前的软删重复行清理 ---------- +-- 唯一索引 (product_id, code, deleted) 对「同 code 多条软删行」同样冲突 +--(deleted 值相同=1)。物模型是声明式数据,多余软删行无价值,物理清理: +DELETE p FROM product_property p JOIN product_property k + ON k.product_id = p.product_id AND k.code = p.code AND k.id < p.id WHERE p.deleted = 1; +DELETE e FROM product_event e JOIN product_event k + ON k.product_id = e.product_id AND k.code = e.code AND k.id < e.id WHERE e.deleted = 1; +DELETE a FROM product_action a JOIN product_action k + ON k.product_id = a.product_id AND k.code = a.code AND k.id < a.id WHERE a.deleted = 1; +DELETE p FROM product p JOIN product k + ON k.platform = p.platform AND k.platform_product_id = p.platform_product_id + AND k.id < p.id WHERE p.deleted = 1 AND p.platform_product_id IS NOT NULL; +-- ---------- 8) 唯一索引(统一放在软删重复行清理之后,避免创建失败)---------- +ALTER TABLE product ADD UNIQUE INDEX uk_platform_product (platform, platform_product_id, deleted); +ALTER TABLE product_property ADD UNIQUE INDEX uk_product_code (product_id, code, deleted); +ALTER TABLE product_event ADD UNIQUE INDEX uk_product_code (product_id, code, deleted); +ALTER TABLE product_action ADD UNIQUE INDEX uk_product_code (product_id, code, deleted); diff --git a/deploy-all.bat b/deploy-all.bat index e497fc7..8ca3291 100644 --- a/deploy-all.bat +++ b/deploy-all.bat @@ -116,7 +116,8 @@ if not "!CHK!"=="0" ( goto :eof ) echo 应用 %1 ... -type "%MIG_DIR%\%1" | %PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql -uroot -p'IoT.p2026.Xk9Qm' iot_platform" +REM --force: 迁移脚本中"已存在"的 DDL(重复列/索引)属正常情况,不应中断整体部署 +type "%MIG_DIR%\%1" | %PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql --force -uroot -p'IoT.p2026.Xk9Qm' iot_platform" if !ERRORLEVEL! NEQ 0 ( echo [错误] 迁移 %1 执行失败。 & pause & exit /b 1 ) %PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql -uroot -p'IoT.p2026.Xk9Qm' iot_platform -e \"INSERT IGNORE INTO db_migrations_applied(name) VALUES('%1')\"" 2>nul goto :eof