package cn.iocoder.yudao.module.iot.service.thingmodel; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelBoolOrEnumDataSpecs; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDataSpecs; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; /** * IoT 产品物模型 Service 实现类 * * @author 芋道源码 */ @Service @Validated @Slf4j public class IotThingModelServiceImpl implements IotThingModelService { @Resource private IotThingModelMapper thingModelMapper; @Resource @Lazy // 延迟加载,解决循环依赖 private IotProductService productService; @Resource @Lazy // 延迟加载,解决循环依赖 private IotDeviceModbusPointService deviceModbusPointService; @Override @Transactional(rollbackFor = Exception.class) public Long createThingModel(IotThingModelSaveReqVO createReqVO) { // 1.1 校验功能标识符在同一产品下是否唯一 validateIdentifierUnique(null, createReqVO.getProductId(), createReqVO.getIdentifier()); // 1.2 功能名称在同一产品下是否唯一 validateNameUnique(createReqVO.getProductId(), createReqVO.getName()); // 1.3 校验产品状态,发布状态下,不允许新增功能 validateProductStatus(createReqVO.getProductId()); // 2. 插入数据库 IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(createReqVO); thingModelMapper.insert(thingModel); // 3. 删除缓存 deleteThingModelListCache(createReqVO.getProductId()); return thingModel.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateThingModel(IotThingModelSaveReqVO updateReqVO) { // 1.1 校验功能是否存在 validateProductThingModelMapperExists(updateReqVO.getId()); // 1.2 校验功能标识符是否唯一 validateIdentifierUnique(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier()); // 1.3 校验产品状态,发布状态下,不允许操作功能 validateProductStatus(updateReqVO.getProductId()); // 2. 更新数据库 IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); thingModelMapper.updateById(thingModel); // 3. 同步更新 Modbus 点位的冗余字段(identifier、name) deviceModbusPointService.updateDeviceModbusPointByThingModel( updateReqVO.getId(), updateReqVO.getIdentifier(), updateReqVO.getName()); // 4. 删除缓存 deleteThingModelListCache(updateReqVO.getProductId()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteThingModel(Long id) { // 1.1 校验功能是否存在 IotThingModelDO thingModel = thingModelMapper.selectById(id); if (thingModel == null) { throw exception(THING_MODEL_NOT_EXISTS); } // 1.2 校验产品状态,发布状态下,不允许操作功能 validateProductStatus(thingModel.getProductId()); // 2. 删除功能 thingModelMapper.deleteById(id); // 3. 删除缓存 deleteThingModelListCache(thingModel.getProductId()); } @Override public IotThingModelDO getThingModel(Long id) { return thingModelMapper.selectById(id); } @Override public List getThingModelListByProductId(Long productId) { return thingModelMapper.selectListByProductId(productId); } @Override public List getThingModelListByProductIdAndIdentifiers(Long productId, Collection identifiers) { return thingModelMapper.selectListByProductIdAndIdentifiers(productId, identifiers); } @Override public List getThingModelListByProductIdAndType(Long productId, Integer type) { return thingModelMapper.selectListByProductIdAndType(productId, type); } @Override @Cacheable(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productId") @TenantIgnore // 忽略租户信息 public List getThingModelListByProductIdFromCache(Long productId) { return thingModelMapper.selectListByProductId(productId); } @Override public PageResult getProductThingModelPage(IotThingModelPageReqVO pageReqVO) { return thingModelMapper.selectPage(pageReqVO); } @Override public List getThingModelList(IotThingModelListReqVO reqVO) { return thingModelMapper.selectList(reqVO); } @Override public void validateThingModelListExists(Long productId, Set identifiers) { if (CollUtil.isEmpty(identifiers)) { return; } List thingModels = thingModelMapper.selectListByProductIdAndIdentifiers( productId, identifiers); Set foundIdentifiers = convertSet(thingModels, IotThingModelDO::getIdentifier); for (String identifier : identifiers) { if (!foundIdentifiers.contains(identifier)) { throw exception(THING_MODEL_NOT_EXISTS); } } } @Override public Object convertThingModelPropertyValue(IotThingModelDO thingModel, Object value) { if (thingModel == null || thingModel.getProperty() == null || value == null) { return null; } String dataType = thingModel.getProperty().getDataType(); if (ObjectUtils.equalsAny(dataType, IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,没有对应数据类型,只能通过 JSON 来存储 return convertToVarchar(JsonUtils.toJsonString(value), TDengineTableField.LENGTH_VARCHAR); } if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) { return convertToInt(value); } if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) { return convertToFloat(value); } if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) { return convertToDouble(value); } if (IotDataSpecsDataTypeEnum.ENUM.getDataType().equals(dataType)) { return convertEnumToTinyInt(thingModel, value); } if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) { return convertBoolToTinyInt(value); } if (IotDataSpecsDataTypeEnum.TEXT.getDataType().equals(dataType)) { return convertToVarchar(Convert.toStr(value), getTextLength(thingModel)); } if (IotDataSpecsDataTypeEnum.DATE.getDataType().equals(dataType)) { return convertToTimestamp(value); } return null; } private Integer getTextLength(IotThingModelDO thingModel) { ThingModelDataSpecs dataSpecs = thingModel.getProperty().getDataSpecs(); if (!(dataSpecs instanceof ThingModelDateOrTextDataSpecs)) { return null; } return ((ThingModelDateOrTextDataSpecs) dataSpecs).getLength(); } private String convertToVarchar(String value, Integer length) { if (value == null) { return null; } if (length != null && value.getBytes(StandardCharsets.UTF_8).length > length) { return null; } return value; } private Integer convertToInt(Object value) { BigDecimal decimal = convertToBigDecimal(value); if (decimal == null) { return null; } try { return decimal.intValueExact(); } catch (ArithmeticException e) { return null; } } private Float convertToFloat(Object value) { BigDecimal decimal = convertToBigDecimal(value); if (decimal == null) { return null; } float result = decimal.floatValue(); return Float.isFinite(result) ? result : null; } private Double convertToDouble(Object value) { BigDecimal decimal = convertToBigDecimal(value); if (decimal == null) { return null; } double result = decimal.doubleValue(); return Double.isFinite(result) ? result : null; } private BigDecimal convertToBigDecimal(Object value) { if (value instanceof Boolean) { return null; } String text = Convert.toStr(value); if (StrUtil.isBlank(text)) { return null; } try { return new BigDecimal(text); } catch (NumberFormatException e) { return null; } } private Byte convertEnumToTinyInt(IotThingModelDO thingModel, Object value) { Integer intValue = convertToInt(value); if (intValue == null && value instanceof CharSequence) { intValue = getEnumValueByName(thingModel, value.toString()); } if (intValue == null || !isTinyInt(intValue)) { return null; } if (CollUtil.isNotEmpty(thingModel.getProperty().getDataSpecsList()) && getEnumDataSpecsByValue(thingModel, intValue) == null) { return null; } return intValue.byteValue(); } private Integer getEnumValueByName(IotThingModelDO thingModel, String name) { ThingModelBoolOrEnumDataSpecs dataSpecs = getEnumDataSpecsByName(thingModel, name); return dataSpecs != null ? dataSpecs.getValue() : null; } private ThingModelBoolOrEnumDataSpecs getEnumDataSpecsByName(IotThingModelDO thingModel, String name) { if (CollUtil.isEmpty(thingModel.getProperty().getDataSpecsList())) { return null; } for (ThingModelDataSpecs dataSpecs : thingModel.getProperty().getDataSpecsList()) { if (!(dataSpecs instanceof ThingModelBoolOrEnumDataSpecs)) { continue; } ThingModelBoolOrEnumDataSpecs enumDataSpecs = (ThingModelBoolOrEnumDataSpecs) dataSpecs; if (StrUtil.equals(enumDataSpecs.getName(), name)) { return enumDataSpecs; } } return null; } private ThingModelBoolOrEnumDataSpecs getEnumDataSpecsByValue(IotThingModelDO thingModel, Integer value) { if (CollUtil.isEmpty(thingModel.getProperty().getDataSpecsList())) { return null; } for (ThingModelDataSpecs dataSpecs : thingModel.getProperty().getDataSpecsList()) { if (!(dataSpecs instanceof ThingModelBoolOrEnumDataSpecs)) { continue; } ThingModelBoolOrEnumDataSpecs enumDataSpecs = (ThingModelBoolOrEnumDataSpecs) dataSpecs; if (Objects.equals(enumDataSpecs.getValue(), value)) { return enumDataSpecs; } } return null; } private Byte convertBoolToTinyInt(Object value) { if (value instanceof Boolean) { return (Boolean) value ? (byte) 1 : (byte) 0; } if (value instanceof CharSequence) { String text = StrUtil.trim(value.toString()); if (StrUtil.equalsIgnoreCase(text, "true")) { return (byte) 1; } if (StrUtil.equalsIgnoreCase(text, "false")) { return (byte) 0; } } Integer intValue = convertToInt(value); if (intValue == null || (intValue != 0 && intValue != 1)) { return null; } return intValue.byteValue(); } private boolean isTinyInt(Integer value) { return value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE; } private Long convertToTimestamp(Object value) { if (value instanceof LocalDateTime) { return LocalDateTimeUtil.toEpochMilli((LocalDateTime) value); } if (value instanceof LocalDate) { return LocalDateTimeUtil.toEpochMilli(((LocalDate) value).atStartOfDay()); } if (value instanceof Instant) { return ((Instant) value).toEpochMilli(); } if (value instanceof OffsetDateTime) { return ((OffsetDateTime) value).toInstant().toEpochMilli(); } if (value instanceof ZonedDateTime) { return ((ZonedDateTime) value).toInstant().toEpochMilli(); } if (value instanceof Date) { return ((Date) value).getTime(); } Long timestamp = convertToLong(value); if (timestamp != null) { return timestamp; } if (!(value instanceof CharSequence)) { return null; } String text = StrUtil.trim(value.toString()); if (StrUtil.isBlank(text)) { return null; } try { return OffsetDateTime.parse(text).toInstant().toEpochMilli(); } catch (Exception ignored) { // 尝试本地时间格式,例如 yyyy-MM-dd HH:mm:ss } try { return LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.parse(text, DatePattern.NORM_DATETIME_PATTERN)); } catch (Exception ignored) { // 尝试其它 Hutool 支持的本地时间格式 } try { return LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtils.parse(text)); } catch (Exception ignored) { return null; } } private Long convertToLong(Object value) { BigDecimal decimal = convertToBigDecimal(value); if (decimal == null) { return null; } try { return decimal.longValueExact(); } catch (ArithmeticException e) { return null; } } private void validateProductThingModelMapperExists(Long id) { if (thingModelMapper.selectById(id) == null) { throw exception(THING_MODEL_NOT_EXISTS); } } private void validateIdentifierUnique(Long id, Long productId, String identifier) { // 1. 情况一:创建时校验 if (id == null) { // 1.1 系统保留字段,不能用于标识符定义 if (StrUtil.equalsAny(identifier, "set", "get", "post", "property", "event", "time", "value")) { throw exception(THING_MODEL_IDENTIFIER_INVALID); } // 1.2 校验唯一 IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); if (thingModel != null) { throw exception(THING_MODEL_IDENTIFIER_EXISTS); } return; } // 2. 情况二:更新时校验 IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); if (thingModel != null && ObjectUtil.notEqual(thingModel.getId(), id)) { throw exception(THING_MODEL_IDENTIFIER_EXISTS); } } private void validateProductStatus(Long createReqVO) { IotProductDO product = productService.validateProductExists(createReqVO); if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) { throw exception(PRODUCT_STATUS_NOT_ALLOW_THING_MODEL); } } private void validateNameUnique(Long productId, String name) { IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndName(productId, name); if (thingModel != null) { throw exception(THING_MODEL_NAME_EXISTS); } } private void deleteThingModelListCache(Long productId) { // 保证 Spring AOP 触发 getSelf().deleteThingModelListCache0(productId); } @CacheEvict(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productId") @TenantIgnore // 忽略租户信息 public void deleteThingModelListCache0(Long productId) { } private IotThingModelServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } }