【功能完善】IoT:设备新增、修改支持更多字段

This commit is contained in:
YunaiV 2024-12-14 16:12:24 +08:00
parent db9c485285
commit afaf98c44f
13 changed files with 249 additions and 147 deletions

View File

@ -0,0 +1,58 @@
package cn.iocoder.yudao.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
/**
* Set<Long> 的类型转换器实现类对应数据库的 varchar 类型
*
* @author 芋道源码
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class LongSetTypeHandler implements TypeHandler<Set<Long>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, Set<Long> strings, JdbcType jdbcType) throws SQLException {
// 设置占位符
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public Set<Long> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public Set<Long> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public Set<Long> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private Set<Long> getResult(String value) {
if (value == null) {
return null;
}
return StrUtils.splitToLongSet(value, COMMA);
}
}

View File

@ -26,9 +26,9 @@ public interface ErrorCodeConstants {
ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在");
ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一");
ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除");
ErrorCode DEVICE_NAME_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_003, "设备名称不能修改");
ErrorCode DEVICE_PRODUCT_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_004, "产品不能修改");
ErrorCode DEVICE_INVALID_DEVICE_STATUS = new ErrorCode(1_050_003_005, "无效的设备状态");
ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在");
ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在");
ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备");
// ========== 产品分类 1-050-004-000 ==========
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");

View File

@ -36,4 +36,14 @@ public enum IotProductDeviceTypeEnum implements IntArrayValuable {
return ARRAYS;
}
/**
* 判断是否是网关
*
* @param type 类型
* @return 是否是网关
*/
public static boolean isGateway(Integer type) {
return GATEWAY.getType().equals(type);
}
}

View File

@ -18,7 +18,10 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@Tag(name = "管理后台 - IoT 设备")
@RestController
@ -86,4 +89,14 @@ public class IotDeviceController {
return success(deviceService.getDeviceCountByProductId(productId));
}
@GetMapping("/simple-list")
@Operation(summary = "获取设备的精简信息列表", description = "主要用于前端的下拉选项")
@Parameter(name = "deviceType", description = "设备类型", example = "1")
public CommonResult<List<IotDeviceRespVO>> getSimpleDeviceList(
@RequestParam(value = "deviceType", required = false) Integer deviceType) {
List<IotDeviceDO> list = deviceService.getDeviceList(deviceType);
return success(convertList(list, device -> // 只返回 idname 字段
new IotDeviceRespVO().setId(device.getId()).setDeviceName(device.getDeviceName())));
}
}

View File

@ -10,13 +10,25 @@ public class IotDeviceSaveReqVO {
@Schema(description = "设备编号", example = "177")
private Long id;
@Schema(description = "设备编号", requiredMode = Schema.RequiredMode.AUTO, example = "177")
private String deviceKey;
@Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
private String deviceName;
@Schema(description = "备注名称", example = "张三")
private String nickname;
@Schema(description = "设备序列号", example = "123456")
private String serialNumber;
@Schema(description = "设备图片", example = "https://iocoder.cn/1.png")
private String picUrl;
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202")
private Long productId;
@Schema(description = "网关设备 ID", example = "16380")
private Long gatewayId;
}

View File

@ -121,12 +121,12 @@ public class IotProductController {
}
@GetMapping("/simple-list")
@Operation(summary = "获得所有产品列表")
@PreAuthorize("@ss.hasPermission('iot:product:query')")
@Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项")
public CommonResult<List<IotProductRespVO>> getSimpleProductList() {
List<IotProductDO> list = productService.getProductList();
return success(convertList(list, product -> // 只返回 idname 字段
new IotProductRespVO().setId(product.getId()).setName(product.getName())));
new IotProductRespVO().setId(product.getId()).setName(product.getName())
.setDeviceType(product.getDeviceType())));
}
}

View File

@ -38,8 +38,8 @@ public class IotProductRespVO {
@ExcelProperty("产品图标")
private String icon;
@Schema(description = "产品图", example = "https://iocoder.cn/1.png")
@ExcelProperty("产品图")
@Schema(description = "产品图", example = "https://iocoder.cn/1.png")
@ExcelProperty("产品图")
private String picUrl;
@Schema(description = "产品描述", example = "你猜")

View File

@ -28,7 +28,7 @@ public class IotProductSaveReqVO {
@Schema(description = "产品图标", example = "https://iocoder.cn/1.svg")
private String icon;
@Schema(description = "产品图", example = "https://iocoder.cn/1.png")
@Schema(description = "产品图", example = "https://iocoder.cn/1.png")
private String picUrl;
@Schema(description = "产品描述", example = "描述")

View File

@ -1,22 +1,25 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.device;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Set;
/**
* IoT 设备 DO
*
* @author haohao
*/
@TableName("iot_device")
@TableName(value = "iot_device", autoResultMap = true)
@KeySequence("iot_device_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ -47,6 +50,17 @@ public class IotDeviceDO extends BaseDO {
* 设备序列号
*/
private String serialNumber;
/**
* 设备图片
*/
private String picUrl;
/**
* 设备分组编号集合
*
* 关联 TODO 芋艿
*/
@TableField(typeHandler = LongSetTypeHandler.class)
private Set<Long> groupIds;
/**
* 产品编号
@ -66,13 +80,6 @@ public class IotDeviceDO extends BaseDO {
* 冗余 {@link IotProductDO#getDeviceType()}
*/
private Integer deviceType;
/**
* 设备状态
* <p>
* 枚举 {@link IotDeviceStatusEnum}
*/
private Integer status;
/**
* 网关设备编号
* <p>
@ -82,6 +89,13 @@ public class IotDeviceDO extends BaseDO {
*/
private Long gatewayId;
/**
* 设备状态
* <p>
* 枚举 {@link IotDeviceStatusEnum}
*/
private Integer status;
/**
* 设备状态最后更新时间
*/

View File

@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePa
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* IoT 设备 Mapper
*
@ -51,4 +53,13 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
default Long selectCountByProductId(Long productId) {
return selectCount(IotDeviceDO::getProductId, productId);
}
default IotDeviceDO selectByDeviceKey(String deviceKey) {
return selectOne(IotDeviceDO::getDeviceKey, deviceKey);
}
default List<IotDeviceDO> selectList(Integer deviceType) {
return selectList(IotDeviceDO::getDeviceType, deviceType);
}
}

View File

@ -21,7 +21,7 @@ public interface IotProductCategoryMapper extends BaseMapperX<IotProductCategory
return selectPage(reqVO, new LambdaQueryWrapperX<IotProductCategoryDO>()
.likeIfPresent(IotProductCategoryDO::getName, reqVO.getName())
.betweenIfPresent(IotProductCategoryDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(IotProductCategoryDO::getId));
.orderByAsc(IotProductCategoryDO::getSort));
}
default List<IotProductCategoryDO> selectListByStatus(Integer status) {

View File

@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.iot.service.device;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceStatusUpdateReqVO;
import jakarta.validation.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import jakarta.validation.Valid;
import javax.annotation.Nullable;
import java.util.List;
/**
* IoT 设备 Service 接口
@ -52,6 +55,14 @@ public interface IotDeviceService {
*/
PageResult<IotDeviceDO> getDevicePage(IotDevicePageReqVO pageReqVO);
/**
* 获得设备列表
*
* @param deviceType 设备类型
* @return 设备列表
*/
List<IotDeviceDO> getDeviceList(@Nullable Integer deviceType);
/**
* 更新设备状态
*
@ -75,4 +86,5 @@ public interface IotDeviceService {
* @return 设备信息
*/
IotDeviceDO getDeviceByProductKeyAndDeviceName(String productKey, String deviceName);
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.iot.service.device;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
@ -12,18 +12,17 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper;
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.security.SecureRandom;
import javax.annotation.Nullable;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
@ -40,142 +39,64 @@ public class IotDeviceServiceImpl implements IotDeviceService {
@Resource
private IotDeviceMapper deviceMapper;
@Resource
private IotProductService productService;
/**
* 创建 IoT 设备
*
* @param createReqVO 创建请求 VO
* @return 设备 ID
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Long createDevice(IotDeviceSaveReqVO createReqVO) {
// 1.1 校验产品是否存在
IotProductDO product = productService.getProduct(createReqVO.getProductId());
if (product == null) {
throw exception(PRODUCT_NOT_EXISTS);
}
// 1.2 校验设备名称在同一产品下是否唯一
if (StrUtil.isBlank(createReqVO.getDeviceName())) {
createReqVO.setDeviceName(generateUniqueDeviceName(product.getProductKey()));
} else {
validateDeviceNameUnique(product.getProductKey(), createReqVO.getDeviceName());
// 1.2 校验设备标识是否唯一
if (deviceMapper.selectByDeviceKey(createReqVO.getDeviceKey()) != null) {
throw exception(DEVICE_KEY_EXISTS);
}
// 1.3 校验设备名称在同一产品下是否唯一
if (deviceMapper.selectByProductKeyAndDeviceName(product.getProductKey(), createReqVO.getDeviceKey()) != null) {
throw exception(DEVICE_NAME_EXISTS);
}
// 1.4 校验父设备是否为合法网关
if (IotProductDeviceTypeEnum.isGateway(product.getDeviceType())
&& createReqVO.getGatewayId() != null) {
validateGatewayDeviceExists(createReqVO.getGatewayId());
}
// 2.1 转换 VO DO
IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class)
.setProductKey(product.getProductKey())
.setDeviceType(product.getDeviceType());
// 2.2 生成并设置必要的字段
device.setDeviceKey(generateUniqueDeviceKey());
device.setDeviceSecret(generateDeviceSecret());
device.setMqttClientId(generateMqttClientId());
device.setMqttUsername(generateMqttUsername(device.getDeviceName(), device.getProductKey()));
device.setMqttPassword(generateMqttPassword());
// 2.3 设置设备状态为未激活
device.setStatus(IotDeviceStatusEnum.INACTIVE.getStatus());
device.setStatusLastUpdateTime(LocalDateTime.now());
// 2.4 插入到数据库
IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class, o -> {
o.setProductKey(product.getProductKey()).setDeviceType(product.getDeviceType());
// 生成并设置必要的字段
o.setDeviceSecret(generateDeviceSecret())
.setMqttClientId(generateMqttClientId())
.setMqttUsername(generateMqttUsername(o.getDeviceName(), o.getProductKey()))
.setMqttPassword(generateMqttPassword());
// 设置设备状态为未激活
o.setStatus(IotDeviceStatusEnum.INACTIVE.getStatus()).setStatusLastUpdateTime(LocalDateTime.now());
});
// 2.2 插入到数据库
deviceMapper.insert(device);
return device.getId();
}
/**
* 校验设备名称在同一产品下是否唯一
*
* @param productKey 产品 Key
* @param deviceName 设备名称
*/
private void validateDeviceNameUnique(String productKey, String deviceName) {
IotDeviceDO existingDevice = deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName);
if (existingDevice != null) {
throw exception(DEVICE_NAME_EXISTS);
}
}
/**
* 生成唯一的 deviceKey
*
* @return 生成的 deviceKey
*/
private String generateUniqueDeviceKey() {
return UUID.randomUUID().toString();
}
/**
* 生成 deviceSecret
*
* @return 生成的 deviceSecret
*/
private String generateDeviceSecret() {
return IdUtil.fastSimpleUUID();
}
/**
* 生成 MQTT Client ID
*
* @return 生成的 MQTT Client ID
*/
private String generateMqttClientId() {
return UUID.randomUUID().toString();
}
/**
* 生成 MQTT Username
*
* @param deviceName 设备名称
* @param productKey 产品 Key
* @return 生成的 MQTT Username
*/
private String generateMqttUsername(String deviceName, String productKey) {
return deviceName + "&" + productKey;
}
/**
* 生成 MQTT Password
*
* @return 生成的 MQTT Password
*/
private String generateMqttPassword() {
// TODO @浩浩这里的 StrUtil 随机字符串
SecureRandom secureRandom = new SecureRandom();
byte[] passwordBytes = new byte[32]; // 256 位的随机数
secureRandom.nextBytes(passwordBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(passwordBytes);
}
/**
* 生成唯一的 DeviceName
*
* @param productKey 产品标识
* @return 生成的唯一 DeviceName
*/
private String generateUniqueDeviceName(String productKey) {
for (int i = 0; i < Short.MAX_VALUE; i++) {
String deviceName = IdUtil.fastSimpleUUID().substring(0, 20);
if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) {
return deviceName;
}
}
throw new IllegalArgumentException("生成 DeviceName 失败");
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateDevice(IotDeviceSaveReqVO updateReqVO) {
// 1. 校验存在
validateDeviceExists(updateReqVO.getId());
updateReqVO.setDeviceKey(null).setDeviceName(null).setProductId(null); // 不允许更新
// 1.1 校验存在
IotDeviceDO device = validateDeviceExists(updateReqVO.getId());
// 1.2 校验父设备是否为合法网关
if (IotProductDeviceTypeEnum.isGateway(device.getDeviceType())
&& updateReqVO.getGatewayId() != null) {
validateGatewayDeviceExists(updateReqVO.getGatewayId());
}
// 2. 更新到数据库
IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class)
.setDeviceName(null).setProductId(null); // 设备名称 产品 ID 不能修改
IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class);
deviceMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteDevice(Long id) {
// 1.1 校验存在
IotDeviceDO device = validateDeviceExists(id);
@ -202,13 +123,24 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return device;
}
@Override
public IotDeviceDO getDevice(Long id) {
/**
* 校验网关设备是否存在
*
* @param id 设备 ID
*/
private void validateGatewayDeviceExists(Long id) {
IotDeviceDO device = deviceMapper.selectById(id);
if (device == null) {
throw exception(DEVICE_NOT_EXISTS);
throw exception(DEVICE_GATEWAY_NOT_EXISTS);
}
return device;
if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) {
throw exception(DEVICE_NOT_GATEWAY);
}
}
@Override
public IotDeviceDO getDevice(Long id) {
return deviceMapper.selectById(id);
}
@Override
@ -216,19 +148,24 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return deviceMapper.selectPage(pageReqVO);
}
@Override
public List<IotDeviceDO> getDeviceList(@Nullable Integer deviceType) {
return deviceMapper.selectList(deviceType);
}
@Override
public void updateDeviceStatus(IotDeviceStatusUpdateReqVO updateReqVO) {
// 1. 校验存在
IotDeviceDO device = validateDeviceExists(updateReqVO.getId());
// 2.1 更新状态和更新时间
IotDeviceDO updateDevice = BeanUtils.toBean(updateReqVO, IotDeviceDO.class);
IotDeviceDO updateDevice = BeanUtils.toBean(updateReqVO, IotDeviceDO.class)
.setStatusLastUpdateTime(LocalDateTime.now());
// 2.2 更新状态相关时间
if (Objects.equals(device.getStatus(), IotDeviceStatusEnum.INACTIVE.getStatus())
&& Objects.equals(updateDevice.getStatus(), IotDeviceStatusEnum.ONLINE.getStatus())) {
// 从未激活到在线设置激活时间和最后上线时间
updateDevice.setActiveTime(LocalDateTime.now());
updateDevice.setLastOnlineTime(LocalDateTime.now());
updateDevice.setActiveTime(LocalDateTime.now()).setLastOnlineTime(LocalDateTime.now());
} else if (Objects.equals(updateDevice.getStatus(), IotDeviceStatusEnum.ONLINE.getStatus())) {
// 如果是上线设置最后上线时间
updateDevice.setLastOnlineTime(LocalDateTime.now());
@ -236,10 +173,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
// 如果是离线设置最后离线时间
updateDevice.setLastOfflineTime(LocalDateTime.now());
}
// 2.3 设置状态更新时间
updateDevice.setStatusLastUpdateTime(LocalDateTime.now());
// 2.4 更新到数据库
// 2.3 更新到数据库
deviceMapper.updateById(updateDevice);
}
@ -254,4 +188,42 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName);
}
/**
* 生成 deviceSecret
*
* @return 生成的 deviceSecret
*/
private String generateDeviceSecret() {
return IdUtil.fastSimpleUUID();
}
/**
* 生成 MQTT Client ID
*
* @return 生成的 MQTT Client ID
*/
private String generateMqttClientId() {
return IdUtil.fastSimpleUUID();
}
/**
* 生成 MQTT Username
*
* @param deviceName 设备名称
* @param productKey 产品 Key
* @return 生成的 MQTT Username
*/
private String generateMqttUsername(String deviceName, String productKey) {
return deviceName + "&" + productKey;
}
/**
* 生成 MQTT Password
*
* @return 生成的 MQTT Password
*/
private String generateMqttPassword() {
return RandomUtil.randomString(32);
}
}