From 9786286ad339eb635c26437df329f03604f70e94 Mon Sep 17 00:00:00 2001 From: 2995840376 <2995840376@xiaomi.com> Date: Tue, 27 Oct 2026 12:22:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(B1):=20=E4=BF=AE=E5=A4=8D=20resolveProductI?= =?UTF-8?q?d=20=E5=8F=82=E6=95=B0=E9=94=99=E4=BD=8D=E6=B1=A1=E6=9F=93?= =?UTF-8?q?=E4=BA=A7=E5=93=81=E5=AD=97=E6=AE=B5=20+=20product=5Fkey=20?= =?UTF-8?q?=E7=A1=AE=E5=AE=9A=E6=80=A7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: TuyaController 两处调用把「涂鸦 productId」填进 model 位、把字面量 'tuya' 填进 manufacturer 位, 导致 product.model / manufacturer 被写入 平台产品ID (生产数据已出现 P_tuya_y1ouxiqr_42bff218 的脏记录)。 修复: - TuyaController: 2 处调用改为 model=null, manufacturer=null (涂鸦 OpenAPI 确实未提供真实型号/厂商, 此前是错位而非真实值) - ProductServiceImpl.resolveProductId: 新增 sanitizeDimension() 防御, 对畸形入参(等于平台产品ID / 等于平台编码)统一置空, 杜绝同类错位复发 - resolveProductId 入口 platform 经 PlatformType.normalize() 归一 product_key 确定性化: - 旧: P___<随机UUID8> -> 不可读、无法反查 - 新: P__ -> 恒定、可反查 (超 56 字符截断并追加短哈希防碰撞) 验证: mvn -pl iot-common,iot-device -am compile 通过 --- .../iot/device/controller/TuyaController.java | 6 +- .../service/impl/ProductServiceImpl.java | 57 +++++++++++++++++-- 2 files changed, 56 insertions(+), 7 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 fa0e8f4..c050537 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 @@ -355,8 +355,10 @@ public class TuyaController { ? tuyaDevice.getIcon() : existing.getIcon()); update.setRemark("涂鸦设备"); if (existing.getProductId() == null) { + // model / manufacturer 传 null:涂鸦 OpenAPI 未提供真实型号与厂商, + // 历史误把 productId 填进 model 位,污染了 product 表的型号字段。 update.setProductId(productService.resolveProductId("tuya", tuyaDevice.getProductId(), - tuyaDevice.getProductId(), "tuya", tuyaDevice.getCategory(), tuyaDevice.getDeviceName())); + null, null, tuyaDevice.getCategory(), tuyaDevice.getDeviceName())); } deviceMapper.updateById(update); count++; @@ -382,7 +384,7 @@ public class TuyaController { localDevice.setIcon(tuyaDevice.getIcon()); localDevice.setRemark("涂鸦设备"); localDevice.setProductId(productService.resolveProductId("tuya", tuyaDevice.getProductId(), - tuyaDevice.getProductId(), "tuya", tuyaDevice.getCategory(), tuyaDevice.getDeviceName())); + null, null, tuyaDevice.getCategory(), tuyaDevice.getDeviceName())); deviceMapper.insert(localDevice); count++; } diff --git a/backend/iot-device/src/main/java/com/iot/device/service/impl/ProductServiceImpl.java b/backend/iot-device/src/main/java/com/iot/device/service/impl/ProductServiceImpl.java index bdd17fd..0142d6a 100644 --- a/backend/iot-device/src/main/java/com/iot/device/service/impl/ProductServiceImpl.java +++ b/backend/iot-device/src/main/java/com/iot/device/service/impl/ProductServiceImpl.java @@ -2,6 +2,7 @@ package com.iot.device.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.iot.common.constants.PlatformType; import com.iot.device.dto.ProductSpec; import com.iot.device.entity.Device; import com.iot.device.entity.Product; @@ -42,7 +43,7 @@ public class ProductServiceImpl implements ProductService { @Transactional public Product createProduct(Product product) { if (!StringUtils.hasText(product.getProductKey())) { - product.setProductKey(genKey(product.getPlatform(), product.getModel())); + product.setProductKey(genKey(product.getPlatform(), product.getPlatformProductId(), product.getModel())); } if (product.getSpecVersion() == null) { product.setSpecVersion(1); @@ -198,6 +199,13 @@ public class ProductServiceImpl implements ProductService { @Transactional public Long resolveProductId(String platform, String platformProductId, String model, String manufacturer, String category, String name) { + platform = PlatformType.normalize(platform); + // 防错位:历史调用方曾把「涂鸦 productId」填进 model 位、 + // 把字面量 "tuya" 填进 manufacturer 位,导致 product 表型号/厂商被污染。 + // 这里对明显畸形的入参做归一,杜绝同类问题复发。 + model = sanitizeDimension(model, platformProductId); + manufacturer = sanitizeDimension(manufacturer, platformProductId); + if (StringUtils.hasText(platformProductId)) { Product p = productMapper.selectOne(new LambdaQueryWrapper() .eq(Product::getPlatform, platform) @@ -222,13 +230,30 @@ public class ProductServiceImpl implements ProductService { np.setManufacturer(manufacturer); np.setCategory(category); np.setName(StringUtils.hasText(name) ? name : (StringUtils.hasText(model) ? model : "未命名产品")); - np.setProductKey(genKey(platform, model)); + np.setProductKey(genKey(platform, platformProductId, model)); np.setStatus("published"); np.setSpecVersion(1); productMapper.insert(np); return np.getId(); } + /** + * 清洗型号/厂商入参:与平台产品ID相同、或等于平台编码的,判定为历史错位传入,置空。 + */ + private String sanitizeDimension(String value, String platformProductId) { + if (!StringUtils.hasText(value)) { + return null; + } + String v = value.trim(); + if (StringUtils.hasText(platformProductId) && v.equals(platformProductId.trim())) { + return null; + } + if (v.equalsIgnoreCase("tuya") || v.equalsIgnoreCase("local") || v.equalsIgnoreCase("matter")) { + return null; + } + return v; + } + @Override @Transactional public int reconcileDevices() { @@ -338,8 +363,30 @@ public class ProductServiceImpl implements ProductService { return (n != null && !n.isNull()) ? n.asText() : null; } - private String genKey(String platform, String model) { - String base = "P_" + (platform == null ? "" : platform) + "_" + (model == null ? "" : model); - return (base + "_" + UUID.randomUUID().toString().substring(0, 8)).replaceAll("\\s", ""); + /** + * 生成产品业务编码。 + * + *

确定性:同一平台同一产品恒定得到同一编码,可从编码反查平台与平台产品ID。 + * 历史实现使用随机 UUID 后缀(如 {@code P_tuya__8a50357a}),既不可读也无法反查。

+ * + *

优先级:平台产品ID > 型号 > 品类;均缺失时用短随机串兜底(此时无法保证确定性)。

+ */ + private String genKey(String platform, String platformProductId, String model) { + String suffix; + if (StringUtils.hasText(platformProductId)) { + suffix = platformProductId.trim(); + } else if (StringUtils.hasText(model)) { + suffix = model.trim(); + } else { + suffix = UUID.randomUUID().toString().substring(0, 8); + } + String base = "P_" + (StringUtils.hasText(platform) ? platform.trim() : "unknown") + + "_" + suffix.replaceAll("\\s", ""); + // product_key 列长 64,超长时截断并追加短哈希以降低碰撞概率 + if (base.length() > 56) { + String h = Integer.toHexString(base.hashCode()); + base = base.substring(0, 47) + "_" + h; + } + return base; } }