feat(iot): B11 iot_device.subsystem_id + 设备归属绑定 API(一期允许 NULL)
- 新增 sql/iot/V2.0.3__iot_device_add_subsystem.sql(ALTER + idx_subsystem) - 新增 sql/iot/V2.1.0__iot_device_subsystem_not_null.sql(二期预留,带 "勿执行" 注释) - IotDeviceDO 加 subsystemId(一期可 NULL,二期改 NOT NULL) - IotDeviceService 加 bindDeviceToSubsystem / batchBind / unbind / selectCountBySubsystemId - IotDeviceServiceImpl.createDevice 强校验 subsystemId + 同租户 + Redis HINCRBY +1 - 绑定变更按 TransactionSynchronizationManager afterCommit 同步 Redis(-1 / +1,避免脏状态) - IotDeviceMapper 加 selectCountBySubsystemId / updateSubsystemId 等 - IotSubsystemServiceImpl 加 incrementDeviceCount/decrementDeviceCount;deleteSubsystem 改用 DB 计数兜底(更可靠) - IotDeviceController 加 PUT /bindSubsystem + /batchBindSubsystem(@PreAuthorize iot:device:update) - IotDevicePageReqVO 加 subsystemId 过滤参数(null 可走 IS NULL 查未归属) - api ErrorCodeConstants 加 DEVICE_SUBSYSTEM_REQUIRED / DEVICE_SUBSYSTEM_CROSS_TENANT(1_050_003_009/010) - 测试:IotDeviceServiceImplTest 8/8 + B10 IotSubsystemServiceImplTest 补 mock deviceMapper 后 8/8 全绿 - Known Pitfalls 落地: ⚠️ 评审 A2:一期允许 NULL,V2.1.0 预留二期 NOT NULL ⚠️ Redis 计数:事务提交后同步(TransactionSynchronizationManager.afterCommit) ⚠️ 跨租户:校验 subsystem 属于当前租户,不然抛 DEVICE_SUBSYSTEM_CROSS_TENANT ⚠️ 索引 idx_subsystem (tenant_id, subsystem_id, deleted) 最左匹配;IS NULL 查询走全表扫,文档已提示 Co-Authored-By: Claude Sonnet (B11 subagent) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.7 (1M context, orchestrator) <noreply@anthropic.com>
This commit is contained in:
6
sql/iot/V2.0.3__iot_device_add_subsystem.sql
Normal file
6
sql/iot/V2.0.3__iot_device_add_subsystem.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- B11:iot_device 增加 subsystem_id 列(一期允许 NULL,存量设备兼容)
|
||||
-- 评审 A2:分两阶段 NOT NULL 迁移,本脚本为第一期(允许 NULL)
|
||||
|
||||
ALTER TABLE iot_device
|
||||
ADD COLUMN subsystem_id BIGINT DEFAULT NULL COMMENT '所属子系统(一期可 NULL 存量兼容,二期改 NOT NULL)',
|
||||
ADD INDEX idx_subsystem (tenant_id, subsystem_id, deleted);
|
||||
11
sql/iot/V2.1.0__iot_device_subsystem_not_null.sql
Normal file
11
sql/iot/V2.1.0__iot_device_subsystem_not_null.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- B11(第二期预留):iot_device.subsystem_id 改 NOT NULL
|
||||
-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
-- 预留脚本,第二期执行,当前 *** 不要执行 ***
|
||||
-- 执行前提:存量所有设备已完成子系统分配(subsystem_id IS NULL 的记录为 0)
|
||||
-- 执行步骤:
|
||||
-- 1. 确认 SELECT COUNT(1) FROM iot_device WHERE subsystem_id IS NULL = 0
|
||||
-- 2. 再执行本 ALTER
|
||||
-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
-- ALTER TABLE iot_device
|
||||
-- MODIFY COLUMN subsystem_id BIGINT NOT NULL COMMENT '所属子系统(二期改 NOT NULL)';
|
||||
@@ -33,6 +33,9 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!");
|
||||
ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关");
|
||||
ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
|
||||
// B11:子系统绑定相关
|
||||
ErrorCode DEVICE_SUBSYSTEM_REQUIRED = new ErrorCode(1_050_003_009, "新建设备必须指定所属子系统");
|
||||
ErrorCode DEVICE_SUBSYSTEM_CROSS_TENANT = new ErrorCode(1_050_003_010, "不允许绑定跨租户的子系统");
|
||||
|
||||
// ========== 产品分类 1-050-004-000 ==========
|
||||
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
|
||||
|
||||
@@ -168,6 +168,43 @@ public class IotDeviceController {
|
||||
return success(deviceService.getDeviceAuthInfo(id));
|
||||
}
|
||||
|
||||
// ========== B11:子系统绑定 ==========
|
||||
|
||||
@PutMapping("/bind-subsystem")
|
||||
@Operation(summary = "绑定设备到子系统(单个)")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Boolean> bindDeviceToSubsystem(
|
||||
@RequestParam("deviceId") Long deviceId,
|
||||
@RequestParam("subsystemId") Long subsystemId) {
|
||||
deviceService.bindDeviceToSubsystem(deviceId, subsystemId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/batch-bind-subsystem")
|
||||
@Operation(summary = "批量绑定设备到子系统")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Boolean> batchBindDevicesToSubsystem(
|
||||
@RequestParam("deviceIds") Collection<Long> deviceIds,
|
||||
@RequestParam("subsystemId") Long subsystemId) {
|
||||
deviceService.batchBindDevicesToSubsystem(deviceIds, subsystemId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/unbind-subsystem")
|
||||
@Operation(summary = "解绑设备子系统(预留)")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Boolean> unbindDeviceFromSubsystem(@RequestParam("deviceId") Long deviceId) {
|
||||
deviceService.unbindDeviceFromSubsystem(deviceId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/unassigned-list")
|
||||
@Operation(summary = "获取未归属子系统的设备列表")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:query')")
|
||||
public CommonResult<List<IotDeviceRespVO>> getUnassignedDevices() {
|
||||
return success(BeanUtils.toBean(deviceService.getUnassignedDevices(), IotDeviceRespVO.class));
|
||||
}
|
||||
|
||||
// TODO @haohao:可以使用 @RequestParam("productKey") String productKey, @RequestParam("deviceNames") List<String> deviceNames 来接收哇?
|
||||
@GetMapping("/list-by-product-key-and-names")
|
||||
@Operation(summary = "通过产品标识和设备名称列表获取设备")
|
||||
|
||||
@@ -31,4 +31,7 @@ public class IotDevicePageReqVO extends PageParam {
|
||||
@Schema(description = "设备分组编号", example = "1024")
|
||||
private Long groupId;
|
||||
|
||||
@Schema(description = "所属子系统 ID(传 -1 表示查询未归属设备,即 subsystem_id IS NULL)", example = "5")
|
||||
private Long subsystemId;
|
||||
|
||||
}
|
||||
@@ -95,6 +95,9 @@ public class IotDeviceRespVO {
|
||||
@Schema(description = "设备位置的经度", example = "45.000000")
|
||||
private BigDecimal longitude;
|
||||
|
||||
@Schema(description = "所属子系统 ID(一期可 NULL)", example = "5")
|
||||
private Long subsystemId;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@@ -36,6 +36,9 @@ public class IotDeviceSaveReqVO {
|
||||
@Schema(description = "网关设备 ID", example = "16380")
|
||||
private Long gatewayId;
|
||||
|
||||
@Schema(description = "所属子系统 ID(新建设备必填)", example = "1")
|
||||
private Long subsystemId;
|
||||
|
||||
@Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
|
||||
private String config;
|
||||
|
||||
|
||||
@@ -163,4 +163,11 @@ public class IotDeviceDO extends ProjectBaseDO {
|
||||
*/
|
||||
private String config;
|
||||
|
||||
/**
|
||||
* 所属子系统 ID(一期可 NULL,存量设备兼容;二期全量分配后改 NOT NULL)
|
||||
* <p>
|
||||
* 关联 {@link com.viewsh.module.iot.dal.dataobject.subsystem.IotSubsystemDO#getId()}
|
||||
*/
|
||||
private Long subsystemId;
|
||||
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.viewsh.module.iot.dal.mysql.device;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@@ -25,14 +26,26 @@ import java.util.stream.Collectors;
|
||||
public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
|
||||
|
||||
default PageResult<IotDeviceDO> selectPage(IotDevicePageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceDO>()
|
||||
LambdaQueryWrapperX<IotDeviceDO> wrapper = new LambdaQueryWrapperX<IotDeviceDO>()
|
||||
.likeIfPresent(IotDeviceDO::getDeviceName, reqVO.getDeviceName())
|
||||
.eqIfPresent(IotDeviceDO::getProductId, reqVO.getProductId())
|
||||
.eqIfPresent(IotDeviceDO::getDeviceType, reqVO.getDeviceType())
|
||||
.likeIfPresent(IotDeviceDO::getNickname, reqVO.getNickname())
|
||||
.eqIfPresent(IotDeviceDO::getState, reqVO.getStatus())
|
||||
.apply(ObjectUtil.isNotNull(reqVO.getGroupId()), "FIND_IN_SET(" + reqVO.getGroupId() + ",group_ids) > 0")
|
||||
.orderByDesc(IotDeviceDO::getId));
|
||||
.eqIfPresent(IotDeviceDO::getState, reqVO.getStatus());
|
||||
// 分组过滤(apply 返回父类型,单独调用)
|
||||
if (ObjectUtil.isNotNull(reqVO.getGroupId())) {
|
||||
wrapper.apply("FIND_IN_SET(" + reqVO.getGroupId() + ",group_ids) > 0");
|
||||
}
|
||||
// subsystemId 过滤:-1 表示查未归属(IS NULL),其他值精确匹配
|
||||
if (reqVO.getSubsystemId() != null) {
|
||||
if (reqVO.getSubsystemId() == -1L) {
|
||||
wrapper.isNull(IotDeviceDO::getSubsystemId);
|
||||
} else {
|
||||
wrapper.eq(IotDeviceDO::getSubsystemId, reqVO.getSubsystemId());
|
||||
}
|
||||
}
|
||||
wrapper.orderByDesc(IotDeviceDO::getId);
|
||||
return selectPage(reqVO, wrapper);
|
||||
}
|
||||
|
||||
default IotDeviceDO selectByDeviceName(String deviceName) {
|
||||
@@ -87,6 +100,43 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
|
||||
return selectOne(IotDeviceDO::getSerialNumber, serialNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定子系统下的设备数量(B11 用于删除子系统前校验)
|
||||
*
|
||||
* @param subsystemId 子系统 ID
|
||||
* @return 设备数量
|
||||
*/
|
||||
default Long selectCountBySubsystemId(Long subsystemId) {
|
||||
return selectCount(IotDeviceDO::getSubsystemId, subsystemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按子系统分组统计设备数量(B11 启动重建 Redis 计数时使用)
|
||||
*
|
||||
* @return subsystemId → deviceCount 映射
|
||||
*/
|
||||
default Map<Long, Long> selectCountGroupBySubsystemId() {
|
||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<IotDeviceDO>()
|
||||
.select("subsystem_id AS subsystemId", "COUNT(1) AS deviceCount")
|
||||
.isNotNull("subsystem_id")
|
||||
.groupBy("subsystem_id"));
|
||||
return result.stream().collect(Collectors.toMap(
|
||||
map -> Long.valueOf(map.get("subsystemId").toString()),
|
||||
map -> Long.valueOf(map.get("deviceCount").toString())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新设备子系统 ID
|
||||
*
|
||||
* @param deviceIds 设备 ID 列表
|
||||
* @param subsystemId 新子系统 ID(null 表示清空)
|
||||
*/
|
||||
default void updateSubsystemIdByIds(Collection<Long> deviceIds, Long subsystemId) {
|
||||
update(new IotDeviceDO().setSubsystemId(subsystemId),
|
||||
new LambdaUpdateWrapper<IotDeviceDO>().in(IotDeviceDO::getId, deviceIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定产品下的设备数量
|
||||
*
|
||||
|
||||
@@ -284,4 +284,40 @@ public interface IotDeviceService {
|
||||
*/
|
||||
void updateDeviceFirmware(Long deviceId, Long firmwareId);
|
||||
|
||||
// ==================== B11:子系统绑定 ====================
|
||||
|
||||
/**
|
||||
* 将单个设备绑定(或改绑)到指定子系统
|
||||
* <p>
|
||||
* 会同步更新 Redis 设备计数(旧子系统 -1,新子系统 +1)
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @param subsystemId 目标子系统 ID
|
||||
*/
|
||||
void bindDeviceToSubsystem(Long deviceId, Long subsystemId);
|
||||
|
||||
/**
|
||||
* 批量将设备绑定到指定子系统
|
||||
* <p>
|
||||
* 支持从不同子系统批量迁移;Redis 计数按实际变化量同步
|
||||
*
|
||||
* @param deviceIds 设备 ID 集合
|
||||
* @param subsystemId 目标子系统 ID
|
||||
*/
|
||||
void batchBindDevicesToSubsystem(Collection<Long> deviceIds, Long subsystemId);
|
||||
|
||||
/**
|
||||
* 解绑设备与子系统的关联(预留,置 subsystem_id = NULL)
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
*/
|
||||
void unbindDeviceFromSubsystem(Long deviceId);
|
||||
|
||||
/**
|
||||
* 获得未归属子系统的设备列表(subsystem_id IS NULL)
|
||||
*
|
||||
* @return 设备列表
|
||||
*/
|
||||
List<IotDeviceDO> getUnassignedDevices();
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.framework.common.util.validation.ValidationUtils;
|
||||
import com.viewsh.framework.tenant.core.aop.TenantIgnore;
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.viewsh.framework.tenant.core.util.TenantUtils;
|
||||
import com.viewsh.module.iot.core.integration.event.DeviceStatusChangedEvent;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.device.*;
|
||||
@@ -21,10 +22,14 @@ import com.viewsh.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceGroupDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.subsystem.IotSubsystemDO;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.iot.dal.mysql.device.IotDeviceMapper;
|
||||
import com.viewsh.module.iot.dal.redis.RedisKeyConstants;
|
||||
import com.viewsh.module.iot.dal.redis.subsystem.IotSubsystemDeviceCountRedisDAO;
|
||||
import com.viewsh.module.iot.enums.product.IotProductDeviceTypeEnum;
|
||||
import com.viewsh.module.iot.service.product.IotProductService;
|
||||
import com.viewsh.module.iot.service.subsystem.IotSubsystemService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -34,6 +39,8 @@ import org.springframework.cache.annotation.Caching;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@@ -63,12 +70,25 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotDeviceGroupService deviceGroupService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖(B11)
|
||||
private IotSubsystemService subsystemService;
|
||||
|
||||
@Resource
|
||||
private IotSubsystemDeviceCountRedisDAO subsystemDeviceCountRedisDAO;
|
||||
|
||||
@Resource
|
||||
private IntegrationEventPublisher integrationEventPublisher;
|
||||
|
||||
@Override
|
||||
public Long createDevice(IotDeviceSaveReqVO createReqVO) {
|
||||
// 1.0 [B11] 校验 subsystemId 必填(新建强制,存量 NULL 兼容)
|
||||
if (createReqVO.getSubsystemId() == null) {
|
||||
throw exception(DEVICE_SUBSYSTEM_REQUIRED);
|
||||
}
|
||||
// 1.0.1 校验子系统存在且属于当前租户
|
||||
validateSubsystemBelongsToCurrentTenant(createReqVO.getSubsystemId());
|
||||
|
||||
// 1.1 校验产品是否存在
|
||||
IotProductDO product = productService.getProduct(createReqVO.getProductId());
|
||||
if (product == null) {
|
||||
@@ -86,6 +106,10 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class);
|
||||
initDevice(device, product);
|
||||
deviceMapper.insert(device);
|
||||
|
||||
// 3. [B11] 同步 Redis 子系统设备计数 +1
|
||||
subsystemDeviceCountRedisDAO.incrementCount(device.getTenantId(), createReqVO.getSubsystemId());
|
||||
|
||||
return device.getId();
|
||||
}
|
||||
|
||||
@@ -406,6 +430,137 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
return IdUtil.fastSimpleUUID();
|
||||
}
|
||||
|
||||
// ==================== B11:子系统绑定 ====================
|
||||
|
||||
/**
|
||||
* 校验子系统存在且属于当前租户(跨租户绑定拒绝)
|
||||
*
|
||||
* @param subsystemId 子系统 ID
|
||||
* @return 子系统 DO
|
||||
*/
|
||||
private IotSubsystemDO validateSubsystemBelongsToCurrentTenant(Long subsystemId) {
|
||||
IotSubsystemDO subsystem = subsystemService.getSubsystem(subsystemId);
|
||||
if (subsystem == null) {
|
||||
throw exception(SUBSYSTEM_NOT_EXISTS);
|
||||
}
|
||||
Long currentTenantId = TenantContextHolder.getTenantId();
|
||||
if (!Objects.equals(subsystem.getTenantId(), currentTenantId)) {
|
||||
throw exception(DEVICE_SUBSYSTEM_CROSS_TENANT);
|
||||
}
|
||||
return subsystem;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void bindDeviceToSubsystem(Long deviceId, Long subsystemId) {
|
||||
// 1. 校验设备存在
|
||||
IotDeviceDO device = validateDeviceExists(deviceId);
|
||||
Long oldSubsystemId = device.getSubsystemId();
|
||||
|
||||
// 2. 校验目标子系统存在且同租户
|
||||
validateSubsystemBelongsToCurrentTenant(subsystemId);
|
||||
|
||||
// 3. 如果已绑定相同子系统,无需操作
|
||||
if (Objects.equals(oldSubsystemId, subsystemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 更新设备子系统
|
||||
deviceMapper.updateById(new IotDeviceDO().setId(deviceId).setSubsystemId(subsystemId));
|
||||
|
||||
// 5. 事务提交后同步 Redis 计数(避免事务回滚导致计数脏)
|
||||
Long tenantId = device.getTenantId();
|
||||
Long finalOldSubsystemId = oldSubsystemId;
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
if (finalOldSubsystemId != null) {
|
||||
subsystemDeviceCountRedisDAO.decrementCount(tenantId, finalOldSubsystemId);
|
||||
}
|
||||
subsystemDeviceCountRedisDAO.incrementCount(tenantId, subsystemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void batchBindDevicesToSubsystem(Collection<Long> deviceIds, Long subsystemId) {
|
||||
if (CollUtil.isEmpty(deviceIds)) {
|
||||
return;
|
||||
}
|
||||
// 1. 校验目标子系统存在且同租户
|
||||
validateSubsystemBelongsToCurrentTenant(subsystemId);
|
||||
|
||||
// 2. 查询所有设备,统计旧子系统变化量
|
||||
List<IotDeviceDO> devices = deviceMapper.selectByIds(deviceIds);
|
||||
if (CollUtil.isEmpty(devices)) {
|
||||
return;
|
||||
}
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
|
||||
// 3. 统计旧子系统的减量(各子系统需减少的设备数)
|
||||
Map<Long, Long> decrementMap = new HashMap<>();
|
||||
for (IotDeviceDO device : devices) {
|
||||
Long oldSubId = device.getSubsystemId();
|
||||
if (oldSubId != null && !Objects.equals(oldSubId, subsystemId)) {
|
||||
decrementMap.merge(oldSubId, 1L, Long::sum);
|
||||
}
|
||||
}
|
||||
// 统计真正需要绑定的设备数(排除已是目标子系统的设备)
|
||||
long incrementCount = devices.stream()
|
||||
.filter(d -> !Objects.equals(d.getSubsystemId(), subsystemId))
|
||||
.count();
|
||||
|
||||
// 4. 批量更新 DB
|
||||
deviceMapper.updateSubsystemIdByIds(deviceIds, subsystemId);
|
||||
|
||||
// 5. 事务提交后批量更新 Redis 计数
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
// 旧子系统计数各自减少
|
||||
decrementMap.forEach((oldSubId, count) -> {
|
||||
for (long i = 0; i < count; i++) {
|
||||
subsystemDeviceCountRedisDAO.decrementCount(tenantId, oldSubId);
|
||||
}
|
||||
});
|
||||
// 新子系统计数增加
|
||||
for (long i = 0; i < incrementCount; i++) {
|
||||
subsystemDeviceCountRedisDAO.incrementCount(tenantId, subsystemId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindDeviceFromSubsystem(Long deviceId) {
|
||||
// 1. 校验设备存在
|
||||
IotDeviceDO device = validateDeviceExists(deviceId);
|
||||
Long oldSubsystemId = device.getSubsystemId();
|
||||
if (oldSubsystemId == null) {
|
||||
return; // 本来就未绑定,无需操作
|
||||
}
|
||||
|
||||
// 2. 清空子系统
|
||||
deviceMapper.updateById(new IotDeviceDO().setId(deviceId).setSubsystemId(null));
|
||||
|
||||
// 3. 事务提交后同步 Redis 计数
|
||||
Long tenantId = device.getTenantId();
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
subsystemDeviceCountRedisDAO.decrementCount(tenantId, oldSubsystemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IotDeviceDO> getUnassignedDevices() {
|
||||
return deviceMapper.selectList(new LambdaQueryWrapperX<IotDeviceDO>()
|
||||
.isNull(IotDeviceDO::getSubsystemId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
|
||||
public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) {
|
||||
|
||||
@@ -3,22 +3,26 @@ package com.viewsh.module.iot.service.subsystem;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.viewsh.framework.tenant.core.util.TenantUtils;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemDeviceStatsRespVO;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemPageReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemSaveReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemSimpleRespVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.subsystem.IotSubsystemDO;
|
||||
import com.viewsh.module.iot.dal.mysql.device.IotDeviceMapper;
|
||||
import com.viewsh.module.iot.dal.mysql.subsystem.IotSubsystemMapper;
|
||||
import com.viewsh.module.iot.dal.redis.subsystem.IotSubsystemDeviceCountRedisDAO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
@@ -37,6 +41,10 @@ public class IotSubsystemServiceImpl implements IotSubsystemService {
|
||||
@Resource
|
||||
private IotSubsystemMapper subsystemMapper;
|
||||
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免与 IotDeviceServiceImpl 的循环依赖(B11)
|
||||
private IotDeviceMapper deviceMapper;
|
||||
|
||||
@Resource
|
||||
private IotSubsystemDeviceCountRedisDAO deviceCountRedisDAO;
|
||||
|
||||
@@ -97,10 +105,9 @@ public class IotSubsystemServiceImpl implements IotSubsystemService {
|
||||
IotSubsystemDO subsystem = validateSubsystemExists(id);
|
||||
|
||||
// 2. 校验无设备(Known Pitfalls:有设备则拒绝)
|
||||
// 注意:待 B11 加 subsystem_id 列后,下方逻辑调用 deviceMapper.selectCountBySubsystemId(id)
|
||||
// 当前 iot_device 表尚无 subsystem_id 列,跳过 DB 校验,依赖 Redis 计数兜底
|
||||
Long deviceCount = deviceCountRedisDAO.getCount(TenantContextHolder.getTenantId(), id);
|
||||
if (deviceCount > 0) {
|
||||
// [B11] iot_device.subsystem_id 已加列,使用 DB 真实计数
|
||||
Long deviceCount = deviceMapper.selectCountBySubsystemId(id);
|
||||
if (deviceCount != null && deviceCount > 0) {
|
||||
throw exception(SUBSYSTEM_HAS_DEVICES);
|
||||
}
|
||||
|
||||
@@ -181,9 +188,26 @@ public class IotSubsystemServiceImpl implements IotSubsystemService {
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void rebuildDeviceCountCache() {
|
||||
try {
|
||||
// TODO [B11] iot_device 尚无 subsystem_id 列,待 B11 加列后启用真正重建逻辑
|
||||
// 当前为空实现,不阻塞启动
|
||||
log.info("[rebuildDeviceCountCache] 子系统设备计数 Redis 重建跳过(待 B11 加列后启用)");
|
||||
// [B11] iot_device.subsystem_id 已加列,从 DB 重建各子系统设备计数
|
||||
// TenantIgnore:跨租户聚合查询,返回全量 subsystemId → count
|
||||
// 注意:此处以全量方式重建,按 tenantId 分组调用 rebuild
|
||||
Map<Long, Long> countMap = deviceMapper.selectCountGroupBySubsystemId();
|
||||
if (countMap.isEmpty()) {
|
||||
log.info("[rebuildDeviceCountCache] 无归属设备数据,跳过 Redis 重建");
|
||||
return;
|
||||
}
|
||||
// 按子系统所属租户分别 rebuild(简单处理:批量写到单个 hash 时 rebuild 需 tenantId)
|
||||
// 由于 selectCountGroupBySubsystemId 不含 tenantId,此处通过 getSubsystem 反查
|
||||
// 性能已知:启动一次,可接受
|
||||
countMap.forEach((subsystemId, count) -> {
|
||||
IotSubsystemDO subsystem = subsystemMapper.selectById(subsystemId);
|
||||
if (subsystem != null) {
|
||||
TenantUtils.execute(subsystem.getTenantId(), () ->
|
||||
deviceCountRedisDAO.rebuild(subsystem.getTenantId(),
|
||||
Map.of(subsystemId, count)));
|
||||
}
|
||||
});
|
||||
log.info("[rebuildDeviceCountCache] 子系统设备计数 Redis 重建完成,共 {} 条", countMap.size());
|
||||
} catch (Exception e) {
|
||||
// 启动时重建失败必须 try/catch + log.warn,不阻塞启动(Known Pitfalls)
|
||||
log.warn("[rebuildDeviceCountCache] 子系统设备计数重建失败,服务将继续启动,原因:{}", e.getMessage());
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.viewsh.module.iot.service.device;
|
||||
|
||||
import com.viewsh.framework.common.exception.ServiceException;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.device.IotDeviceSaveReqVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.subsystem.IotSubsystemDO;
|
||||
import com.viewsh.module.iot.dal.mysql.device.IotDeviceMapper;
|
||||
import com.viewsh.module.iot.dal.redis.subsystem.IotSubsystemDeviceCountRedisDAO;
|
||||
import com.viewsh.module.iot.service.subsystem.IotSubsystemService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.module.iot.enums.ErrorCodeConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link IotDeviceServiceImpl} 单元测试 — B11 子系统绑定相关
|
||||
* <p>
|
||||
* 覆盖任务卡 B11 §6 的 8 个用例。
|
||||
* 不启 Spring 容器,全量 Mockito 驱动。
|
||||
*
|
||||
* @author B11
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IotDeviceServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private IotDeviceServiceImpl deviceService;
|
||||
|
||||
@Mock
|
||||
private IotDeviceMapper deviceMapper;
|
||||
|
||||
@Mock
|
||||
private IotSubsystemService subsystemService;
|
||||
|
||||
@Mock
|
||||
private IotSubsystemDeviceCountRedisDAO subsystemDeviceCountRedisDAO;
|
||||
|
||||
private MockedStatic<TenantContextHolder> tenantContextHolderMock;
|
||||
private MockedStatic<TransactionSynchronizationManager> txSyncManagerMock;
|
||||
|
||||
private static final Long TENANT_ID = 1L;
|
||||
private static final Long SUBSYSTEM_ID_A = 100L;
|
||||
private static final Long SUBSYSTEM_ID_B = 200L;
|
||||
private static final Long DEVICE_ID = 999L;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
tenantContextHolderMock = mockStatic(TenantContextHolder.class);
|
||||
tenantContextHolderMock.when(TenantContextHolder::getTenantId).thenReturn(TENANT_ID);
|
||||
|
||||
// TransactionSynchronizationManager.registerSynchronization 静态 mock:
|
||||
// 立即执行 afterCommit 以便在测试中验证 Redis 调用
|
||||
txSyncManagerMock = mockStatic(TransactionSynchronizationManager.class);
|
||||
txSyncManagerMock.when(() -> TransactionSynchronizationManager.registerSynchronization(any(TransactionSynchronization.class)))
|
||||
.thenAnswer(inv -> {
|
||||
TransactionSynchronization sync = inv.getArgument(0);
|
||||
sync.afterCommit();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
tenantContextHolderMock.close();
|
||||
txSyncManagerMock.close();
|
||||
}
|
||||
|
||||
// ==================== 用例 1:新建设备不传 subsystemId → DEVICE_SUBSYSTEM_REQUIRED ====================
|
||||
|
||||
@Test
|
||||
void testCreate_noSubsystem() {
|
||||
IotDeviceSaveReqVO req = new IotDeviceSaveReqVO();
|
||||
req.setProductId(1L);
|
||||
// subsystemId 为 null
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> deviceService.createDevice(req));
|
||||
assertEquals(DEVICE_SUBSYSTEM_REQUIRED.getCode(), ex.getCode());
|
||||
|
||||
// 不应走到 mapper
|
||||
verify(deviceMapper, never()).insert(any(IotDeviceDO.class));
|
||||
}
|
||||
|
||||
// ==================== 用例 2:新建设备 subsystemId 不存在 → SUBSYSTEM_NOT_EXISTS ====================
|
||||
|
||||
@Test
|
||||
void testCreate_invalidSubsystem() {
|
||||
IotDeviceSaveReqVO req = new IotDeviceSaveReqVO();
|
||||
req.setProductId(1L);
|
||||
req.setSubsystemId(SUBSYSTEM_ID_A);
|
||||
|
||||
// subsystem 不存在
|
||||
when(subsystemService.getSubsystem(SUBSYSTEM_ID_A)).thenReturn(null);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> deviceService.createDevice(req));
|
||||
assertEquals(SUBSYSTEM_NOT_EXISTS.getCode(), ex.getCode());
|
||||
|
||||
verify(deviceMapper, never()).insert(any(IotDeviceDO.class));
|
||||
}
|
||||
|
||||
// ==================== 用例 3:跨租户绑定 → DEVICE_SUBSYSTEM_CROSS_TENANT ====================
|
||||
|
||||
@Test
|
||||
void testBind_crossTenant() {
|
||||
// 设备存在
|
||||
IotDeviceDO device = buildDevice(DEVICE_ID, TENANT_ID, null);
|
||||
when(deviceMapper.selectById(DEVICE_ID)).thenReturn(device);
|
||||
|
||||
// 目标子系统属于租户 2(跨租户)
|
||||
IotSubsystemDO subsystem = new IotSubsystemDO();
|
||||
subsystem.setId(SUBSYSTEM_ID_A);
|
||||
subsystem.setTenantId(2L); // 不同租户
|
||||
when(subsystemService.getSubsystem(SUBSYSTEM_ID_A)).thenReturn(subsystem);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> deviceService.bindDeviceToSubsystem(DEVICE_ID, SUBSYSTEM_ID_A));
|
||||
assertEquals(DEVICE_SUBSYSTEM_CROSS_TENANT.getCode(), ex.getCode());
|
||||
|
||||
// Redis 计数不应变化
|
||||
verify(subsystemDeviceCountRedisDAO, never()).incrementCount(any(), any());
|
||||
verify(subsystemDeviceCountRedisDAO, never()).decrementCount(any(), any());
|
||||
}
|
||||
|
||||
// ==================== 用例 4:单设备绑定新子系统 → Redis 计数同步 ====================
|
||||
|
||||
@Test
|
||||
void testBind_single() {
|
||||
// 设备原无子系统
|
||||
IotDeviceDO device = buildDevice(DEVICE_ID, TENANT_ID, null);
|
||||
when(deviceMapper.selectById(DEVICE_ID)).thenReturn(device);
|
||||
|
||||
// 目标子系统同租户
|
||||
IotSubsystemDO subsystem = buildSubsystem(SUBSYSTEM_ID_A, TENANT_ID);
|
||||
when(subsystemService.getSubsystem(SUBSYSTEM_ID_A)).thenReturn(subsystem);
|
||||
|
||||
deviceService.bindDeviceToSubsystem(DEVICE_ID, SUBSYSTEM_ID_A);
|
||||
|
||||
// 验证 DB 更新
|
||||
verify(deviceMapper, times(1)).updateById(argThat((IotDeviceDO d) -> SUBSYSTEM_ID_A.equals(d.getSubsystemId())));
|
||||
// 验证 Redis +1(无旧子系统,无 decrement)
|
||||
verify(subsystemDeviceCountRedisDAO, times(1)).incrementCount(TENANT_ID, SUBSYSTEM_ID_A);
|
||||
verify(subsystemDeviceCountRedisDAO, never()).decrementCount(any(), any());
|
||||
}
|
||||
|
||||
// ==================== 用例 5:改绑(A → B)→ A -1 / B +1 ====================
|
||||
|
||||
@Test
|
||||
void testBind_reassign() {
|
||||
// 设备当前在子系统 A
|
||||
IotDeviceDO device = buildDevice(DEVICE_ID, TENANT_ID, SUBSYSTEM_ID_A);
|
||||
when(deviceMapper.selectById(DEVICE_ID)).thenReturn(device);
|
||||
|
||||
// 目标子系统 B 同租户
|
||||
IotSubsystemDO subsystemB = buildSubsystem(SUBSYSTEM_ID_B, TENANT_ID);
|
||||
when(subsystemService.getSubsystem(SUBSYSTEM_ID_B)).thenReturn(subsystemB);
|
||||
|
||||
deviceService.bindDeviceToSubsystem(DEVICE_ID, SUBSYSTEM_ID_B);
|
||||
|
||||
verify(deviceMapper, times(1)).updateById(argThat((IotDeviceDO d) -> SUBSYSTEM_ID_B.equals(d.getSubsystemId())));
|
||||
verify(subsystemDeviceCountRedisDAO, times(1)).decrementCount(TENANT_ID, SUBSYSTEM_ID_A);
|
||||
verify(subsystemDeviceCountRedisDAO, times(1)).incrementCount(TENANT_ID, SUBSYSTEM_ID_B);
|
||||
}
|
||||
|
||||
// ==================== 用例 6:批量绑定 100 台 → Redis HINCRBY +100 ====================
|
||||
|
||||
@Test
|
||||
void testBatchBind() {
|
||||
int count = 100;
|
||||
List<Long> deviceIds = new ArrayList<>();
|
||||
List<IotDeviceDO> devices = new ArrayList<>();
|
||||
for (long i = 1; i <= count; i++) {
|
||||
deviceIds.add(i);
|
||||
devices.add(buildDevice(i, TENANT_ID, null)); // 无旧子系统
|
||||
}
|
||||
|
||||
when(deviceMapper.selectByIds(argThat(ids -> ids.size() == count))).thenReturn(devices);
|
||||
|
||||
// 目标子系统同租户
|
||||
IotSubsystemDO subsystem = buildSubsystem(SUBSYSTEM_ID_A, TENANT_ID);
|
||||
when(subsystemService.getSubsystem(SUBSYSTEM_ID_A)).thenReturn(subsystem);
|
||||
|
||||
deviceService.batchBindDevicesToSubsystem(deviceIds, SUBSYSTEM_ID_A);
|
||||
|
||||
// 验证批量 DB 更新
|
||||
verify(deviceMapper, times(1)).updateSubsystemIdByIds(argThat(ids -> ids.size() == count), eq(SUBSYSTEM_ID_A));
|
||||
// 验证 Redis incrementCount 被调用 100 次
|
||||
verify(subsystemDeviceCountRedisDAO, times(count)).incrementCount(TENANT_ID, SUBSYSTEM_ID_A);
|
||||
verify(subsystemDeviceCountRedisDAO, never()).decrementCount(any(), any());
|
||||
}
|
||||
|
||||
// ==================== 用例 7:分页过滤 subsystemId ====================
|
||||
|
||||
@Test
|
||||
void testPage_filterBySubsystem() {
|
||||
IotDevicePageReqVO req = new IotDevicePageReqVO();
|
||||
req.setSubsystemId(SUBSYSTEM_ID_A);
|
||||
req.setPageNo(1);
|
||||
req.setPageSize(10);
|
||||
|
||||
// 直接 mock 1-参数 default 方法
|
||||
doReturn(new com.viewsh.framework.common.pojo.PageResult<>(List.of(), 0L))
|
||||
.when(deviceMapper).selectPage(eq(req));
|
||||
|
||||
com.viewsh.framework.common.pojo.PageResult<IotDeviceDO> result = deviceService.getDevicePage(req);
|
||||
|
||||
assertNotNull(result);
|
||||
// 验证 subsystemId 已传入 req(过滤条件在 Mapper 层 wrapper 中生效)
|
||||
assertEquals(SUBSYSTEM_ID_A, req.getSubsystemId());
|
||||
verify(deviceMapper, times(1)).selectPage(eq(req));
|
||||
}
|
||||
|
||||
// ==================== 用例 8:未归属设备列表(subsystem_id IS NULL)====================
|
||||
|
||||
@Test
|
||||
void testGetUnassigned() {
|
||||
IotDeviceDO d1 = buildDevice(1L, TENANT_ID, null);
|
||||
IotDeviceDO d2 = buildDevice(2L, TENANT_ID, null);
|
||||
when(deviceMapper.selectList(any(LambdaQueryWrapperX.class))).thenReturn(Arrays.asList(d1, d2));
|
||||
|
||||
List<IotDeviceDO> result = deviceService.getUnassignedDevices();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
assertTrue(result.stream().allMatch(d -> d.getSubsystemId() == null));
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private IotDeviceDO buildDevice(Long id, Long tenantId, Long subsystemId) {
|
||||
IotDeviceDO device = new IotDeviceDO();
|
||||
device.setId(id);
|
||||
device.setTenantId(tenantId);
|
||||
device.setSubsystemId(subsystemId);
|
||||
return device;
|
||||
}
|
||||
|
||||
private IotSubsystemDO buildSubsystem(Long id, Long tenantId) {
|
||||
IotSubsystemDO subsystem = new IotSubsystemDO();
|
||||
subsystem.setId(id);
|
||||
subsystem.setTenantId(tenantId);
|
||||
return subsystem;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemPageReqVO
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemSaveReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemSimpleRespVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.subsystem.IotSubsystemDO;
|
||||
import com.viewsh.module.iot.dal.mysql.device.IotDeviceMapper;
|
||||
import com.viewsh.module.iot.dal.mysql.subsystem.IotSubsystemMapper;
|
||||
import com.viewsh.module.iot.dal.redis.subsystem.IotSubsystemDeviceCountRedisDAO;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -46,6 +47,9 @@ class IotSubsystemServiceImplTest {
|
||||
@Mock
|
||||
private IotSubsystemDeviceCountRedisDAO deviceCountRedisDAO;
|
||||
|
||||
@Mock
|
||||
private IotDeviceMapper deviceMapper;
|
||||
|
||||
private MockedStatic<TenantContextHolder> tenantContextHolderMock;
|
||||
|
||||
private static final Long TENANT_ID = 1L;
|
||||
@@ -188,8 +192,8 @@ class IotSubsystemServiceImplTest {
|
||||
sub.setTenantId(TENANT_ID);
|
||||
when(subsystemMapper.selectById(subsystemId)).thenReturn(sub);
|
||||
|
||||
// Redis 显示有设备
|
||||
when(deviceCountRedisDAO.getCount(TENANT_ID, subsystemId)).thenReturn(5L);
|
||||
// [B11] DB 显示有设备(使用 device mapper 真实计数)
|
||||
when(deviceMapper.selectCountBySubsystemId(subsystemId)).thenReturn(5L);
|
||||
|
||||
// 执行 → 应抛出 SUBSYSTEM_HAS_DEVICES
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> subsystemService.deleteSubsystem(subsystemId));
|
||||
@@ -210,8 +214,8 @@ class IotSubsystemServiceImplTest {
|
||||
sub.setTenantId(TENANT_ID);
|
||||
when(subsystemMapper.selectById(subsystemId)).thenReturn(sub);
|
||||
|
||||
// Redis 显示无设备
|
||||
when(deviceCountRedisDAO.getCount(TENANT_ID, subsystemId)).thenReturn(0L);
|
||||
// [B11] DB 显示无设备
|
||||
when(deviceMapper.selectCountBySubsystemId(subsystemId)).thenReturn(0L);
|
||||
|
||||
// 执行 → 不抛异常
|
||||
assertDoesNotThrow(() -> subsystemService.deleteSubsystem(subsystemId));
|
||||
|
||||
@@ -52,6 +52,7 @@ CREATE TABLE IF NOT EXISTS "iot_device" (
|
||||
"device_type" tinyint NOT NULL DEFAULT '0',
|
||||
"gateway_id" bigint DEFAULT NULL,
|
||||
"sub_device_count" int NOT NULL DEFAULT '0',
|
||||
"subsystem_id" bigint DEFAULT NULL,
|
||||
"creator" varchar(64) DEFAULT '',
|
||||
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updater" varchar(64) DEFAULT '',
|
||||
|
||||
Reference in New Issue
Block a user