chore: IOT模块单体版本 - 代码合并
This commit is contained in:
@@ -47,12 +47,27 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/get") // 特殊:方便调用,暂时使用 POST,实际更推荐 GET
|
||||
@PermitAll
|
||||
public CommonResult<IotDeviceRespDTO> getDevice(@RequestBody IotDeviceGetReqDTO getReqDTO) {
|
||||
IotDeviceDO device = getReqDTO.getId() != null ? deviceService.getDeviceFromCache(getReqDTO.getId())
|
||||
: deviceService.getDeviceFromCache(getReqDTO.getProductKey(), getReqDTO.getDeviceName());
|
||||
IotDeviceDO device;
|
||||
|
||||
// 查询优先级:id > (productKey + deviceName) > deviceName
|
||||
if (getReqDTO.getId() != null) {
|
||||
// 通过设备 ID 查询
|
||||
device = deviceService.getDeviceFromCache(getReqDTO.getId());
|
||||
} else if (getReqDTO.getProductKey() != null && getReqDTO.getDeviceName() != null) {
|
||||
// 通过 productKey + deviceName 查询
|
||||
device = deviceService.getDeviceFromCache(getReqDTO.getProductKey(), getReqDTO.getDeviceName());
|
||||
} else if (getReqDTO.getDeviceName() != null) {
|
||||
// 仅通过 deviceName 查询(用于 JT808 等协议,终端手机号应该是全局唯一的)
|
||||
device = deviceService.getDeviceFromCacheByDeviceName(getReqDTO.getDeviceName());
|
||||
} else {
|
||||
device = null;
|
||||
}
|
||||
|
||||
return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
|
||||
IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
|
||||
if (product != null) {
|
||||
deviceDTO.setCodecType(product.getCodecType());
|
||||
deviceDTO.setProductAuthType(product.getAuthType());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -37,7 +38,7 @@ public class IotAlertConfigController {
|
||||
|
||||
@Resource
|
||||
private IotAlertConfigService alertConfigService;
|
||||
|
||||
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@@ -79,7 +80,7 @@ public class IotAlertConfigController {
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-config:query')")
|
||||
public CommonResult<PageResult<IotAlertConfigRespVO>> getAlertConfigPage(@Valid IotAlertConfigPageReqVO pageReqVO) {
|
||||
PageResult<IotAlertConfigDO> pageResult = alertConfigService.getAlertConfigPage(pageReqVO);
|
||||
|
||||
|
||||
// 转换返回
|
||||
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
|
||||
convertSetByFlatMap(pageResult.getList(), config -> config.getReceiveUserIds().stream()));
|
||||
|
||||
@@ -6,10 +6,10 @@ import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyDetailRespVO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDevicePropertyDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
|
||||
import com.viewsh.module.iot.enums.thingmodel.IotThingModelTypeEnum;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import com.viewsh.module.iot.service.device.property.IotDevicePropertyService;
|
||||
|
||||
@@ -3,9 +3,9 @@ package com.viewsh.module.iot.controller.admin.ota;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.module.iot.controller.admin.ota.vo.task.IotOtaTaskCreateReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.ota.vo.task.IotOtaTaskPageReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.ota.vo.task.IotOtaTaskRespVO;
|
||||
import com.viewsh.module.iot.controller.admin.ota.vo.task.IotOtaTaskCreateReqVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.ota.IotOtaTaskDO;
|
||||
import com.viewsh.module.iot.service.ota.IotOtaTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
@@ -2,17 +2,14 @@ package com.viewsh.module.iot.dal.dataobject.device;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.type.LongSetTypeHandler;
|
||||
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import com.viewsh.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceStateEnum;
|
||||
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.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -127,9 +124,11 @@ public class IotDeviceDO extends TenantBaseDO {
|
||||
*/
|
||||
private String deviceSecret;
|
||||
/**
|
||||
* 认证类型(如一机一密、动态注册)
|
||||
* 认证类型
|
||||
* <p>
|
||||
* 如果为空,则继承产品的 authType
|
||||
* 枚举 {@link com.viewsh.module.iot.core.enums.IotAuthTypeEnum}
|
||||
*/
|
||||
// TODO @haohao:是不是要枚举哈
|
||||
private String authType;
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,10 +4,7 @@ import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* IoT 设备分组 DO
|
||||
|
||||
@@ -4,10 +4,7 @@ import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* IoT 产品分类 DO
|
||||
|
||||
@@ -4,10 +4,7 @@ import com.viewsh.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* IoT 产品 DO
|
||||
@@ -87,4 +84,17 @@ public class IotProductDO extends TenantBaseDO {
|
||||
*/
|
||||
private String codecType;
|
||||
|
||||
/**
|
||||
* 认证类型(认证策略)
|
||||
* <p>
|
||||
* 默认认证方式,可被设备级配置覆盖
|
||||
* 枚举 {@link com.viewsh.module.iot.core.enums.IotAuthTypeEnum}
|
||||
*/
|
||||
private String authType;
|
||||
|
||||
/**
|
||||
* 产品密钥 (一型一密认证需要)
|
||||
*/
|
||||
private String productSecret;
|
||||
|
||||
}
|
||||
@@ -131,6 +131,19 @@ public interface IotDeviceService {
|
||||
*/
|
||||
IotDeviceDO getDeviceFromCache(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 【缓存】根据设备名称获得设备信息(仅通过设备名称查询)
|
||||
* <p>
|
||||
* 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!!
|
||||
* <p>
|
||||
* 此方法主要用于 JT808 等协议,设备在注册时只知道终端手机号(deviceName),
|
||||
* 不知道 productKey。对于 JT808 协议,终端手机号应该是全局唯一的。
|
||||
*
|
||||
* @param deviceName 设备名称(如 JT808 的终端手机号)
|
||||
* @return 设备信息,未找到返回 null
|
||||
*/
|
||||
IotDeviceDO getDeviceFromCacheByDeviceName(String deviceName);
|
||||
|
||||
/**
|
||||
* 获得设备分页
|
||||
*
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.viewsh.framework.tenant.core.aop.TenantIgnore;
|
||||
import com.viewsh.framework.tenant.core.util.TenantUtils;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.device.*;
|
||||
import com.viewsh.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import com.viewsh.module.iot.core.enums.IotAuthTypeEnum;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import com.viewsh.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
@@ -258,6 +259,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
return deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = RedisKeyConstants.DEVICE, key = "'deviceName_' + #deviceName", unless = "#result == null")
|
||||
@TenantIgnore // 忽略租户信息,用于 JT808 等协议,终端手机号应该是全局唯一的
|
||||
public IotDeviceDO getDeviceFromCacheByDeviceName(String deviceName) {
|
||||
return deviceMapper.selectByDeviceName(deviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<IotDeviceDO> getDevicePage(IotDevicePageReqVO pageReqVO) {
|
||||
return deviceMapper.selectPage(pageReqVO);
|
||||
@@ -398,9 +406,31 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
@Override
|
||||
public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) {
|
||||
IotDeviceDO device = validateDeviceExists(id);
|
||||
|
||||
// 获取生效的认证类型
|
||||
String effectiveAuthType = device.getAuthType();
|
||||
String targetSecret = device.getDeviceSecret();
|
||||
|
||||
if (StrUtil.isEmpty(effectiveAuthType)) {
|
||||
IotProductDO product = productService.getProductFromCache(device.getProductId());
|
||||
if (product != null) {
|
||||
effectiveAuthType = product.getAuthType();
|
||||
// 如果是产品级一型一密,需要获取 ProductSecret
|
||||
if (IotAuthTypeEnum.PRODUCT_SECRET.getType().equals(effectiveAuthType)) {
|
||||
targetSecret = product.getProductSecret();
|
||||
}
|
||||
}
|
||||
} else if (IotAuthTypeEnum.PRODUCT_SECRET.getType().equals(effectiveAuthType)) {
|
||||
// 如果设备级强制设为一型一密(罕见但逻辑上兼容)
|
||||
IotProductDO product = productService.getProductFromCache(device.getProductId());
|
||||
if (product != null) {
|
||||
targetSecret = product.getProductSecret();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 IotDeviceAuthUtils 生成认证信息
|
||||
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
|
||||
device.getProductKey(), device.getDeviceName(), targetSecret);
|
||||
return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class);
|
||||
}
|
||||
|
||||
@@ -455,13 +485,68 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
String deviceName = deviceInfo.getDeviceName();
|
||||
String productKey = deviceInfo.getProductKey();
|
||||
IotDeviceDO device = getSelf().getDeviceFromCache(productKey, deviceName);
|
||||
|
||||
// 1.1 处理动态注册 (Dynamic Registration)
|
||||
if (device == null) {
|
||||
// 获取产品信息
|
||||
IotProductDO product = productService.getProductByProductKey(productKey);
|
||||
if (product != null && IotAuthTypeEnum.DYNAMIC.getType().equals(product.getAuthType())) {
|
||||
// 自动创建设备
|
||||
IotDeviceSaveReqVO createReq = new IotDeviceSaveReqVO()
|
||||
.setProductId(product.getId())
|
||||
.setDeviceName(deviceName);
|
||||
try {
|
||||
// TODO: 考虑并发问题,这里简单处理
|
||||
getSelf().createDevice(createReq);
|
||||
// 重新获取设备
|
||||
device = getSelf().getDeviceFromCache(productKey, deviceName);
|
||||
log.info("[authDevice][动态注册设备成功: {}/{}]", productKey, deviceName);
|
||||
} catch (Exception e) {
|
||||
log.error("[authDevice][动态注册设备失败: {}/{}]", productKey, deviceName, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (device == null) {
|
||||
log.warn("[authDevice][设备({}/{}) 不存在]", productKey, deviceName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 校验密码
|
||||
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret());
|
||||
// 2.1 获取生效的认证类型
|
||||
String effectiveAuthType = device.getAuthType();
|
||||
if (StrUtil.isEmpty(effectiveAuthType)) {
|
||||
IotProductDO product = productService.getProductFromCache(device.getProductId());
|
||||
if (product != null) {
|
||||
effectiveAuthType = product.getAuthType();
|
||||
}
|
||||
}
|
||||
|
||||
// 2.2 根据类型校验
|
||||
String targetSecret = device.getDeviceSecret(); // 默认为一机一密
|
||||
|
||||
if (IotAuthTypeEnum.PRODUCT_SECRET.getType().equals(effectiveAuthType)) {
|
||||
// 一型一密:使用 ProductSecret
|
||||
IotProductDO product = productService.getProductFromCache(device.getProductId());
|
||||
if (product == null || StrUtil.isEmpty(product.getProductSecret())) {
|
||||
log.error("[authDevice][一型一密认证失败,ProductSecret 为空: {}]", productKey);
|
||||
return false;
|
||||
}
|
||||
targetSecret = product.getProductSecret();
|
||||
} else if (IotAuthTypeEnum.NONE.getType().equals(effectiveAuthType)) {
|
||||
// 免鉴权:直接通过
|
||||
return true;
|
||||
} else if (IotAuthTypeEnum.DYNAMIC.getType().equals(effectiveAuthType)) {
|
||||
// 动态注册后,通常转为一机一密或一型一密,这里假设动态注册使用一型一密校验
|
||||
// 或者是免鉴权。具体策略视业务而定。这里暂定为一型一密。
|
||||
IotProductDO product = productService.getProductFromCache(device.getProductId());
|
||||
if (product != null) {
|
||||
targetSecret = product.getProductSecret();
|
||||
}
|
||||
}
|
||||
|
||||
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, targetSecret);
|
||||
if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) {
|
||||
log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
|
||||
return false;
|
||||
|
||||
@@ -169,10 +169,13 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
|
||||
getSelf().createDeviceLogAsync(message);
|
||||
|
||||
// 3. 回复消息。前提:非 _reply 消息,并且非禁用回复的消息
|
||||
// 条件1:防止对"回复消息"再次回复,避免无限循环
|
||||
// 条件2:某些特定的method不需要回复(如设备状态变更、OTA进度上报)
|
||||
// 条件3(新增):HTTP短连接场景,因为已经在请求中直接响应了,不需要再通过消息总线发送回复
|
||||
if (IotDeviceMessageUtils.isReplyMessage(message)
|
||||
|| IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())
|
||||
|| StrUtil.isEmpty(message.getServerId())) {
|
||||
return;
|
||||
return; // serverId 为空,不记录回复消息
|
||||
}
|
||||
try {
|
||||
IotDeviceMessage replyMessage = IotDeviceMessage.replyOf(message.getRequestId(), message.getMethod(), replyData,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.iot.service.product;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.framework.tenant.core.aop.TenantIgnore;
|
||||
@@ -53,7 +54,8 @@ public class IotProductServiceImpl implements IotProductService {
|
||||
|
||||
// 2. 插入
|
||||
IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class)
|
||||
.setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus());
|
||||
.setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus())
|
||||
.setProductSecret(IdUtil.fastSimpleUUID()); // 生成 ProductSecret
|
||||
productMapper.insert(product);
|
||||
return product.getId();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
USING device_message
|
||||
TAGS (#{deviceId})
|
||||
VALUES (
|
||||
NOW, #{id}, #{reportTime}, #{tenantId}, #{serverId},
|
||||
#{reportTime}, #{id}, #{reportTime}, #{tenantId}, #{serverId},
|
||||
#{upstream}, #{reply}, #{identifier}, #{requestId}, #{method},
|
||||
#{params}, #{data}, #{code}, #{msg}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user