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; } }