From d7e34efb5825dec401010952440e861b494f7eb4 Mon Sep 17 00:00:00 2001 From: 2995840376 <2995840376@xiaomi.com> Date: Tue, 27 Oct 2026 12:34:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(B2-B4):=20=E4=BA=A7=E5=93=81=E7=94=9F?= =?UTF-8?q?=E5=91=BD=E5=91=A8=E6=9C=9F=E9=97=AD=E7=8E=AF=20-=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E9=98=B2=E5=AD=A4=E5=84=BF=20/=20reconcile=20?= =?UTF-8?q?=E5=B9=82=E7=AD=89=E5=85=A8=E9=87=8F=20/=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E8=87=AA=E6=84=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B2 deleteProduct 引用校验: - 原实现直接 deleteById, 会留下 product_id 指向已删产品的孤儿设备, 且 reconcile 只补 NULL 的设备 => 孤儿永远无法回收 - 现校验 device / firmware / ota_task 三类引用, 任一存在即拒绝删除并提示数量 (此前前端却提示'已关联设备的产品不可删除', 后端实际毫无校验) B3 reconcileDevices 幂等全量对齐: - 原: 仅处理 product_id IS NULL 的设备 - 现: 对所有有 platform_product_id 的设备按 (platform, platform_product_id) 重新解析产品并校正 product_id, 已对齐的跳过(不产生无谓写) - 无平台产品ID的纯本地手工设备跳过 B4 同步自愈 (新增 ProductService.ensureDeviceProduct): - TuyaController 同步路径由「仅当 product_id IS NULL 才解析」改为每次校验 - 产品不存在(含已逻辑删除)/平台产品ID不匹配 => 自动重新解析或创建产品 - 配合 B2 引用校验, 产品删除不再产生不可回收的孤儿 验证: mvn -pl iot-common,iot-device -am compile 通过 --- .../iot/device/controller/TuyaController.java | 14 ++--- .../iot/device/service/ProductService.java | 12 ++++ .../service/impl/ProductServiceImpl.java | 63 ++++++++++++++++++- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/backend/iot-device/src/main/java/com/iot/device/controller/TuyaController.java b/backend/iot-device/src/main/java/com/iot/device/controller/TuyaController.java index c050537..0923be5 100644 --- a/backend/iot-device/src/main/java/com/iot/device/controller/TuyaController.java +++ b/backend/iot-device/src/main/java/com/iot/device/controller/TuyaController.java @@ -354,12 +354,10 @@ public class TuyaController { update.setIcon(tuyaDevice.getIcon() != null ? tuyaDevice.getIcon() : existing.getIcon()); update.setRemark("涂鸦设备"); - if (existing.getProductId() == null) { - // model / manufacturer 传 null:涂鸦 OpenAPI 未提供真实型号与厂商, - // 历史误把 productId 填进 model 位,污染了 product 表的型号字段。 - update.setProductId(productService.resolveProductId("tuya", tuyaDevice.getProductId(), - null, null, tuyaDevice.getCategory(), tuyaDevice.getDeviceName())); - } + // 每次同步都校验产品关联(自愈):产品被删/错挂时自动纠正, + // 不再是「仅当 product_id 为空才解析」——后者会让孤儿设备永远无法回收。 + update.setProductId(productService.ensureDeviceProduct( + existing.getProductId(), "tuya", tuyaDevice.getProductId(), tuyaDevice.getCategory())); deviceMapper.updateById(update); count++; continue; @@ -383,8 +381,8 @@ public class TuyaController { localDevice.setPlatformProductId(tuyaDevice.getProductId()); localDevice.setIcon(tuyaDevice.getIcon()); localDevice.setRemark("涂鸦设备"); - localDevice.setProductId(productService.resolveProductId("tuya", tuyaDevice.getProductId(), - null, null, tuyaDevice.getCategory(), tuyaDevice.getDeviceName())); + localDevice.setProductId(productService.ensureDeviceProduct( + localDevice.getProductId(), "tuya", tuyaDevice.getProductId(), tuyaDevice.getCategory())); deviceMapper.insert(localDevice); count++; } diff --git a/backend/iot-device/src/main/java/com/iot/device/service/ProductService.java b/backend/iot-device/src/main/java/com/iot/device/service/ProductService.java index 8126e25..9ea3a9c 100644 --- a/backend/iot-device/src/main/java/com/iot/device/service/ProductService.java +++ b/backend/iot-device/src/main/java/com/iot/device/service/ProductService.java @@ -43,6 +43,18 @@ public interface ProductService { Long resolveProductId(String platform, String platformProductId, String model, String manufacturer, String category, String name); + /** + * 确保设备关联到正确的产品(同步自愈)。 + * + *
重构前,涂鸦同步只在 {@code device.product_id IS NULL} 时才解析产品, + * 导致产品被删除、设备错挂等情况永远无法自愈。现每次同步都校验:
+ *产品不存在(已被逻辑删除)或平台产品ID不匹配时重新解析, + * 使"产品被删 → 设备成孤儿 → 永不回收"的死结可自愈。
+ */ + @Override + @Transactional + public Long ensureDeviceProduct(Long currentProductId, String platform, String platformProductId, String deviceType) { + if (currentProductId != null && StringUtils.hasText(platformProductId)) { + Product p = productMapper.selectById(currentProductId); + if (p != null && platformProductId.trim().equals(p.getPlatformProductId())) { + return currentProductId; + } + } + return resolveProductId(platform, platformProductId, null, null, deviceType, null); + } + /** * 清洗型号/厂商入参:与平台产品ID相同、或等于平台编码的,判定为历史错位传入,置空。 */ @@ -261,19 +307,30 @@ public class ProductServiceImpl implements ProductService { @Override @Transactional public int reconcileDevices() { + // 重构:原先只处理 product_id IS NULL 的设备,导致两类问题无法自愈 —— + // 1) 产品被删除后,挂在该产品下的设备 product_id 成了孤儿引用,永远不会被修复; + // 2) 设备被错误关联到别的产品后,不会被纠正。 + // 现改为「全量对齐」:对每台有平台产品ID的设备,重新按 (platform, platform_product_id) + // 解析产品并校正 product_id;无平台产品ID的设备(纯本地手工设备)跳过。 List