fix(B1): 修复 resolveProductId 参数错位污染产品字段 + product_key 确定性化

根因: 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_<platform>_<model>_<随机UUID8>  -> 不可读、无法反查
- 新: P_<platform>_<platformProductId|model>  -> 恒定、可反查
  (超 56 字符截断并追加短哈希防碰撞)

验证: mvn -pl iot-common,iot-device -am compile 通过
This commit is contained in:
2995840376 2026-10-27 12:22:39 +08:00
parent a932633c50
commit 9786286ad3
2 changed files with 56 additions and 7 deletions

View File

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

View File

@ -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<Product>()
.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", "");
/**
* 生成产品业务编码。
*
* <p><b>确定性</b>:同一平台同一产品恒定得到同一编码,可从编码反查平台与平台产品ID。
* 历史实现使用随机 UUID 后缀(如 {@code P_tuya__8a50357a}),既不可读也无法反查。</p>
*
* <p>优先级:平台产品ID &gt; 型号 &gt; 品类;均缺失时用短随机串兜底(此时无法保证确定性)。</p>
*/
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;
}
}