chore: IOT模块单体版本 - 代码合并

This commit is contained in:
lzh
2025-12-31 13:32:26 +08:00
parent 8ccfafe2bb
commit 98f5f031a2
50 changed files with 4440 additions and 285 deletions

View File

@@ -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());
}
}));
}

View File

@@ -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()));

View File

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

View File

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

View File

@@ -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;
/**

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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);
/**
* 获得设备分页
*

View File

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

View File

@@ -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,

View File

@@ -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();
}

View File

@@ -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}
)