feat(B11): AI 指令码从产品物模型派生, 消除第三套指令码档案
重构前: ai_device_profile 的指令码全靠品类猜(默认 switch_1/switch_led/control), 与 product_property 物模型、涂鸦 SDK schema 是三套独立来源, 必然漂移 —— 多开关位/带背光设备会拿到错误指令码。 重构后决策优先级: 1. 人工覆盖(source=manual) —— 用户显式指定, 最高优先 2. 产品物模型派生 —— semantic 标注的权威来源(后端 SpecSemanticResolver 落库) 3. 已存在的档案(探测写入/类型默认) —— 兼容历史数据 4. 品类推断兜底 —— 物模型缺失时 新增 ProductSpecMapper(只读投影): 查询产品物模型功能点(code/name/type/semantic/grp/access/dp_id) fromProductSpec 映射: - power -> powerCode (多开关位取 is_primary 通道) - switch -> powerCode 兜底 (物模型未标 power 时取首个可写开关通道) - brightness -> brightnessCode - color_temp -> colorTempCode - mode -> modeCode - temperature -> temperatureCode 派生结果同时写回 product_id(产品级共享), 不再逐设备重复存储 验证: mvn -pl iot-common,iot-device,iot-rule -am compile BUILD SUCCESS
This commit is contained in:
parent
10344e0b05
commit
297206368e
@ -9,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,6 +29,7 @@ public class AiDeviceProfileResolver {
|
|||||||
|
|
||||||
private final AiDeviceProfileMapper aiDeviceProfileMapper;
|
private final AiDeviceProfileMapper aiDeviceProfileMapper;
|
||||||
private final AiEntityResolver aiEntityResolver;
|
private final AiEntityResolver aiEntityResolver;
|
||||||
|
private final com.iot.rule.mapper.ProductSpecMapper productSpecMapper;
|
||||||
|
|
||||||
/** 涂鸦亮度取值区间 */
|
/** 涂鸦亮度取值区间 */
|
||||||
private static final int TUYA_BRIGHT_MIN = 10;
|
private static final int TUYA_BRIGHT_MIN = 10;
|
||||||
@ -40,21 +42,34 @@ public class AiDeviceProfileResolver {
|
|||||||
* @param ownerId 用户ID
|
* @param ownerId 用户ID
|
||||||
*/
|
*/
|
||||||
public AiDeviceProfile resolve(AiDeviceView device, Long ownerId) {
|
public AiDeviceProfile resolve(AiDeviceView device, Long ownerId) {
|
||||||
AiDeviceProfile existing = null;
|
// 决策优先级(重构后):
|
||||||
// 优先产品级档案(同类设备共享),否则回退设备级(兼容旧数据)
|
// 1. 人工覆盖(source=manual) —— 用户显式指定,最高优先
|
||||||
if (device.getProductId() != null) {
|
// 2. 产品物模型派生 —— semantic 标注的权威来源
|
||||||
existing = aiDeviceProfileMapper.selectByProductAndOwner(device.getProductId(), ownerId);
|
// 3. 已存在的档案(探测写入/类型默认) —— 兼容历史数据
|
||||||
|
// 4. 品类推断兜底 —— 物模型缺失时
|
||||||
|
//
|
||||||
|
// 重构前只有 1/3/4 三档,指令码全靠品类猜(如默认 switch_1),
|
||||||
|
// 对多开关位/带背光设备会给出错误指令码 —— 这正是「第三套指令码档案」的危害。
|
||||||
|
|
||||||
|
// 1) 人工覆盖优先
|
||||||
|
AiDeviceProfile manual = loadProfile(device, ownerId);
|
||||||
|
if (manual != null && AiDeviceProfile.SOURCE_MANUAL.equals(manual.getSource())
|
||||||
|
&& manual.getPowerCode() != null && !manual.getPowerCode().isBlank()) {
|
||||||
|
return manual;
|
||||||
}
|
}
|
||||||
if (existing == null) {
|
|
||||||
existing = aiDeviceProfileMapper.selectOne(
|
// 2) 从产品物模型派生
|
||||||
new LambdaQueryWrapper<AiDeviceProfile>()
|
AiDeviceProfile fromSpec = fromProductSpec(device, ownerId);
|
||||||
.eq(AiDeviceProfile::getDeviceId, device.getDeviceId())
|
if (fromSpec != null) {
|
||||||
.eq(AiDeviceProfile::getOwnerId, ownerId)
|
return fromSpec;
|
||||||
.last("limit 1"));
|
|
||||||
}
|
}
|
||||||
if (existing != null && existing.getPowerCode() != null && !existing.getPowerCode().isBlank()) {
|
|
||||||
return existing;
|
// 3) 已存在的档案
|
||||||
|
if (manual != null && manual.getPowerCode() != null && !manual.getPowerCode().isBlank()) {
|
||||||
|
return manual;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4) 品类推断兜底
|
||||||
AiDeviceProfile inferred = infer(device, ownerId);
|
AiDeviceProfile inferred = infer(device, ownerId);
|
||||||
// 落库到产品级(同类设备共享),避免逐设备重复存储
|
// 落库到产品级(同类设备共享),避免逐设备重复存储
|
||||||
if (device.getProductId() != null) {
|
if (device.getProductId() != null) {
|
||||||
@ -69,6 +84,121 @@ public class AiDeviceProfileResolver {
|
|||||||
return inferred;
|
return inferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 读取已有档案:优先产品级,否则设备级(兼容旧数据) */
|
||||||
|
private AiDeviceProfile loadProfile(AiDeviceView device, Long ownerId) {
|
||||||
|
if (device.getProductId() != null) {
|
||||||
|
AiDeviceProfile p = aiDeviceProfileMapper.selectByProductAndOwner(device.getProductId(), ownerId);
|
||||||
|
if (p != null) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aiDeviceProfileMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<AiDeviceProfile>()
|
||||||
|
.eq(AiDeviceProfile::getDeviceId, device.getDeviceId())
|
||||||
|
.eq(AiDeviceProfile::getOwnerId, ownerId)
|
||||||
|
.last("limit 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从产品物模型派生 AI 指令码。
|
||||||
|
*
|
||||||
|
* <p>物模型里的 semantic 由后端 SpecSemanticResolver 落库,是功能点语义的唯一权威:
|
||||||
|
* power → 电源指令码、brightness → 亮度、color_temp → 色温、mode → 模式、
|
||||||
|
* temperature → 温度。多开关位设备取 is_primary 的那个通道。</p>
|
||||||
|
*
|
||||||
|
* @return 派生的档案;物模型缺失或没有可用功能点时返回 null(走品类兜底)
|
||||||
|
*/
|
||||||
|
private AiDeviceProfile fromProductSpec(AiDeviceView device, Long ownerId) {
|
||||||
|
if (device.getProductId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> props;
|
||||||
|
try {
|
||||||
|
props = productSpecMapper.selectProperties(device.getProductId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("读取产品物模型失败(不影响执行): productId={}, {}", device.getProductId(), e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (props == null || props.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String powerCode = null;
|
||||||
|
String brightnessCode = null;
|
||||||
|
String colorTempCode = null;
|
||||||
|
String modeCode = null;
|
||||||
|
String temperatureCode = null;
|
||||||
|
for (Map<String, Object> p : props) {
|
||||||
|
String semantic = str(p.get("semantic"));
|
||||||
|
String code = str(p.get("code"));
|
||||||
|
if (code.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String access = str(p.get("access"));
|
||||||
|
boolean writable = access.contains("write");
|
||||||
|
if (semantic.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (semantic) {
|
||||||
|
case "power":
|
||||||
|
if (powerCode == null || writable) {
|
||||||
|
powerCode = code;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "switch":
|
||||||
|
// 开关通道:物模型未标 power 时,用首个可写开关通道兜底
|
||||||
|
if (powerCode == null && writable) {
|
||||||
|
powerCode = code;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "brightness":
|
||||||
|
if (brightnessCode == null) {
|
||||||
|
brightnessCode = code;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "color_temp":
|
||||||
|
if (colorTempCode == null) {
|
||||||
|
colorTempCode = code;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "mode":
|
||||||
|
if (modeCode == null) {
|
||||||
|
modeCode = code;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "temperature":
|
||||||
|
if (temperatureCode == null) {
|
||||||
|
temperatureCode = code;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (powerCode == null && brightnessCode == null && colorTempCode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
AiDeviceProfile profile = new AiDeviceProfile();
|
||||||
|
profile.setDeviceId(device.getDeviceId());
|
||||||
|
profile.setOwnerId(ownerId);
|
||||||
|
profile.setProductId(device.getProductId());
|
||||||
|
profile.setPowerCode(powerCode);
|
||||||
|
profile.setBrightnessCode(brightnessCode);
|
||||||
|
profile.setColorTempCode(colorTempCode);
|
||||||
|
profile.setModeCode(modeCode);
|
||||||
|
profile.setTemperatureCode(temperatureCode);
|
||||||
|
profile.setPowerOnValue("true");
|
||||||
|
profile.setPowerOffValue("false");
|
||||||
|
profile.setSource(AiDeviceProfile.SOURCE_TYPE_DEFAULT);
|
||||||
|
profile.setEnabled(Boolean.TRUE);
|
||||||
|
log.info("AI指令码从产品物模型派生: deviceId={}, productId={}, powerCode={}",
|
||||||
|
device.getDeviceId(), device.getProductId(), powerCode);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String str(Object v) {
|
||||||
|
return v == null ? "" : String.valueOf(v).trim();
|
||||||
|
}
|
||||||
|
|
||||||
/** 按品类推断默认指令码 */
|
/** 按品类推断默认指令码 */
|
||||||
private AiDeviceProfile infer(AiDeviceView device, Long ownerId) {
|
private AiDeviceProfile infer(AiDeviceView device, Long ownerId) {
|
||||||
String type = aiEntityResolver.resolveDeviceType(device);
|
String type = aiEntityResolver.resolveDeviceType(device);
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.iot.rule.mapper;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品物模型功能点查询(供 AI 指令码从物模型派生)。
|
||||||
|
*
|
||||||
|
* <p>产品物模型由 iot-device 侧的 SpecSemanticResolver 落库,
|
||||||
|
* 是「哪个功能点是什么语义」的唯一权威;AI 控制档案不再靠品类猜指令码。</p>
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ProductSpecMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询产品的全部功能点(含语义标签)。
|
||||||
|
*
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @return 每行含 code / name / type / semantic / grp / access
|
||||||
|
*/
|
||||||
|
@Select("SELECT code, name, type, semantic, grp, access, dp_id FROM product_property "
|
||||||
|
+ "WHERE product_id = #{productId} AND deleted = 0 ORDER BY sort ASC")
|
||||||
|
List<Map<String, Object>> selectProperties(@Param("productId") Long productId);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user