feat(iot): 支持按区域和设备类型查询配置

新增 getConfigByAreaIdAndRelationType 方法,用于跨设备获取配置场景。
例如:工牌设备需要获取该区域的信标配置。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-01-23 13:46:49 +08:00
parent e4d07a5306
commit 06ca3070cd
10 changed files with 1439 additions and 0 deletions

View File

@@ -54,6 +54,19 @@ public interface CleanOrderIntegrationConfigService {
*/
java.util.List<AreaDeviceConfigWrapper> getConfigsByAreaIdAndRelationType(Long areaId, String relationType);
/**
* 根据区域ID和关联类型查询单个配置
* <p>
* 用于跨设备获取配置的场景,例如:工牌设备需要获取该区域的信标配置
* <p>
* 注意:如果同一区域同一类型有多个设备配置,返回第一个
*
* @param areaId 区域ID
* @param relationType 关联类型TRAFFIC_COUNTER/BEACON/BADGE
* @return 集成配置包装器,如果不存在返回 null
*/
AreaDeviceConfigWrapper getConfigByAreaIdAndRelationType(Long areaId, String relationType);
/**
* 清除设备配置缓存
* <p>

View File

@@ -107,6 +107,24 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra
.collect(Collectors.toList());
}
@Override
public AreaDeviceConfigWrapper getConfigByAreaIdAndRelationType(Long areaId, String relationType) {
log.debug("[CleanOrderConfig] 查询单个区域配置areaId={}, relationType={}", areaId, relationType);
List<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType);
if (relations.isEmpty()) {
return null;
}
// 返回第一个启用的配置
return relations.stream()
.filter(r -> r.getEnabled())
.findFirst()
.map(this::wrapConfig)
.orElse(null);
}
@Override
public AreaDeviceConfigWrapper getConfigWrapperByDeviceId(Long deviceId) {
log.debug("[CleanOrderConfig] 查询设备完整配置deviceId={}", deviceId);

View File

@@ -0,0 +1,126 @@
package com.viewsh.module.ops.environment.integration.adapter;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.core.dispatch.model.AssigneeStatus;
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
import java.time.LocalDateTime;
/**
* 工牌设备状态适配器
* <p>
* 将 {@link BadgeDeviceStatusDTO} 适配为通用的 {@link AssigneeStatus} 接口
* <p>
* 设计说明:
* - 适配器模式:将设备状态对象转换为通用接口,供调度引擎使用
* - 设备作为执行人deviceId 作为 assigneeId
* - 解耦设计:调度引擎通过通用接口访问设备状态,不依赖具体实现
*
* @author lzh
*/
public class BadgeDeviceAssigneeStatusAdapter implements AssigneeStatus {
private final BadgeDeviceStatusDTO deviceStatus;
private final Long waitingTaskCount;
public BadgeDeviceAssigneeStatusAdapter(BadgeDeviceStatusDTO deviceStatus) {
this(deviceStatus, 0L);
}
public BadgeDeviceAssigneeStatusAdapter(BadgeDeviceStatusDTO deviceStatus, Long waitingTaskCount) {
this.deviceStatus = deviceStatus;
this.waitingTaskCount = waitingTaskCount != null ? waitingTaskCount : 0L;
}
@Override
public String getStatus() {
return deviceStatus.getStatus() != null ? deviceStatus.getStatus().getCode() : null;
}
@Override
public boolean isIdle() {
return deviceStatus.getStatus() == BadgeDeviceStatusEnum.IDLE;
}
@Override
public boolean isBusy() {
return deviceStatus.getStatus() == BadgeDeviceStatusEnum.BUSY;
}
@Override
public boolean isOnline() {
return deviceStatus.getStatus() != null && deviceStatus.getStatus().isActive();
}
@Override
public Long getCurrentTaskCount() {
// 有正在执行的工单则返回1否则返回0
return deviceStatus.getCurrentOpsOrderId() != null ? 1L : 0L;
}
@Override
public Long getWaitingTaskCount() {
return waitingTaskCount;
}
@Override
public Long getAssigneeId() {
// 设备ID作为执行人ID
return deviceStatus.getDeviceId();
}
@Override
public String getAssigneeName() {
// 设备编码作为执行人名称
return deviceStatus.getDeviceCode();
}
@Override
public Long getAreaId() {
return deviceStatus.getCurrentAreaId();
}
@Override
public LocalDateTime getLastHeartbeatTime() {
// 将时间戳(毫秒)转换为 LocalDateTime
if (deviceStatus.getLastHeartbeatTime() == null) {
return null;
}
return LocalDateTime.ofEpochSecond(deviceStatus.getLastHeartbeatTime() / 1000,
(int) ((deviceStatus.getLastHeartbeatTime() % 1000) * 1_000_000),
java.time.ZoneId.systemDefault().getRules().getOffset(java.time.Instant.EPOCH));
}
@Override
public Integer getBatteryLevel() {
return deviceStatus.getBatteryLevel();
}
@Override
public Object getExtension(String key) {
// 支持业务特定的扩展属性
switch (key) {
case "deviceCode":
return deviceStatus.getDeviceCode();
case "currentAreaName":
return deviceStatus.getCurrentAreaName();
case "currentOrderId":
return deviceStatus.getCurrentOpsOrderId();
case "currentOrderStatus":
return deviceStatus.getCurrentOrderStatus();
case "beaconMac":
return deviceStatus.getBeaconMac();
case "statusChangeTime":
return deviceStatus.getStatusChangeTime();
default:
return null;
}
}
/**
* 获取原始的设备状态对象
*/
public BadgeDeviceStatusDTO getDeviceStatus() {
return deviceStatus;
}
}

View File

@@ -0,0 +1,273 @@
package com.viewsh.module.ops.environment.service.dispatch;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.api.queue.OrderQueueDTO;
import com.viewsh.module.ops.api.queue.OrderQueueService;
import com.viewsh.module.ops.core.dispatch.DispatchEngine;
import com.viewsh.module.ops.core.dispatch.model.AssigneeRecommendation;
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
import com.viewsh.module.ops.core.dispatch.strategy.AssignStrategy;
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 工牌设备区域优先分配策略
* <p>
* 职责:哪个设备来接单
* <p>
* 策略规则:
* 1. 查询该区域的空闲设备(状态=IDLE
* 2. 优先选择电量充足、心跳最新的设备
* 3. 考虑工作量平衡,选择当前任务最少的
*
* @author lzh
*/
@Component
@Slf4j
public class BadgeDeviceAreaAssignStrategy implements AssignStrategy {
private static final String STRATEGY_NAME = "badge_device_area_priority";
private static final String BUSINESS_TYPE = "CLEAN";
@Resource
private BadgeDeviceStatusService badgeDeviceStatusService;
@Resource
private OrderQueueService orderQueueService;
@Resource
private DispatchEngine dispatchEngine;
@PostConstruct
public void init() {
dispatchEngine.registerAssignStrategy(BUSINESS_TYPE, this);
log.info("工牌设备分配策略已注册: strategyName={}, businessType={}", STRATEGY_NAME, BUSINESS_TYPE);
}
@Override
public String getName() {
return STRATEGY_NAME;
}
@Override
public String getSupportedBusinessType() {
return BUSINESS_TYPE;
}
@Override
public AssigneeRecommendation recommend(OrderDispatchContext context) {
log.info("执行工牌设备区域优先分配策略: orderId={}, areaId={}, priority={}",
context.getOrderId(), context.getAreaId(), context.getPriority());
// 查询该区域的设备
List<BadgeDeviceStatusDTO> devices = badgeDeviceStatusService.listBadgesByArea(context.getAreaId());
if (devices.isEmpty()) {
log.warn("该区域没有工牌设备: areaId={}", context.getAreaId());
return AssigneeRecommendation.none();
}
// 过滤在线的设备
List<BadgeDeviceStatusDTO> onlineDevices = devices.stream()
.filter(BadgeDeviceStatusDTO::isOnline)
.collect(Collectors.toList());
if (onlineDevices.isEmpty()) {
log.warn("该区域没有在线工牌设备: areaId={}", context.getAreaId());
return AssigneeRecommendation.none();
}
// 选择最佳设备
BadgeDeviceStatusDTO selectedDevice = selectBestDevice(onlineDevices, context);
if (selectedDevice != null) {
String reason = buildRecommendationReason(selectedDevice, context);
return AssigneeRecommendation.of(
selectedDevice.getDeviceId(),
selectedDevice.getDeviceCode(),
calculateScore(selectedDevice),
reason
);
}
return AssigneeRecommendation.none();
}
@Override
public List<AssigneeRecommendation> recommendBatch(OrderDispatchContext context, int limit) {
log.info("批量推荐工牌设备: areaId={}, priority={}, limit={}",
context.getAreaId(), context.getPriority(), limit);
List<BadgeDeviceStatusDTO> devices = badgeDeviceStatusService.listBadgesByArea(context.getAreaId());
if (devices.isEmpty()) {
return new ArrayList<>();
}
return devices.stream()
.filter(BadgeDeviceStatusDTO::isOnline)
.limit(limit)
.map(device -> {
int score = calculateScore(device);
String reason = buildRecommendationReason(device, context);
return AssigneeRecommendation.of(
device.getDeviceId(),
device.getDeviceCode(),
score,
reason
);
})
.sorted((a, b) -> Integer.compare(b.getScore(), a.getScore()))
.collect(Collectors.toList());
}
// ==================== 私有方法 ====================
/**
* 选择最佳设备
*/
private BadgeDeviceStatusDTO selectBestDevice(List<BadgeDeviceStatusDTO> devices, OrderDispatchContext context) {
// 对于P0紧急任务优先选择IDLE状态的设备
if (context.isUrgent()) {
return devices.stream()
.filter(d -> d.canAcceptNewOrder())
.max(Comparator
.comparing((BadgeDeviceStatusDTO d) -> d.getBatteryLevel() != null && d.getBatteryLevel() > 20 ? 1 : 0)
.thenComparing(BadgeDeviceStatusDTO::getLastHeartbeatTime, Comparator.nullsLast(Comparator.naturalOrder())))
.orElse(null);
}
// 普通任务优先选择IDLE状态其次是BUSY状态等待队列较少的
BadgeDeviceStatusDTO idleDevice = devices.stream()
.filter(d -> d.canAcceptNewOrder())
.max(Comparator
.comparing((BadgeDeviceStatusDTO d) -> d.getBatteryLevel() != null && d.getBatteryLevel() > 20 ? 1 : 0)
.thenComparing(BadgeDeviceStatusDTO::getLastHeartbeatTime, Comparator.nullsLast(Comparator.naturalOrder())))
.orElse(null);
if (idleDevice != null) {
return idleDevice;
}
// 没有空闲设备,选择等待队列较少的忙碌设备
return devices.stream()
.filter(BadgeDeviceStatusDTO::isBusy)
.min(Comparator.comparing(d -> getWaitingTaskCount(d.getDeviceId())))
.orElse(null);
}
/**
* 获取等待任务数量
*/
private int getWaitingTaskCount(Long deviceId) {
try {
// 注意:这里使用 deviceId 作为 userId 查询队列
// 因为 OrderQueueService 是按 userId执行人ID查询的
List<OrderQueueDTO> waitingTasks = orderQueueService.getWaitingTasksByUserId(deviceId);
return waitingTasks != null ? waitingTasks.size() : 0;
} catch (Exception e) {
log.warn("查询等待任务数量失败: deviceId={}", deviceId, e);
return 0;
}
}
/**
* 计算推荐分数0-100
*/
private int calculateScore(BadgeDeviceStatusDTO device) {
int score = 50; // 基础分
// 状态分数
if (device.canAcceptNewOrder()) {
score += 30;
} else if (device.isBusy()) {
score += 10;
}
// 电量分数
if (device.getBatteryLevel() != null) {
if (device.getBatteryLevel() > 80) {
score += 15;
} else if (device.getBatteryLevel() > 50) {
score += 10;
} else if (device.getBatteryLevel() > 20) {
score += 5;
}
}
// 心跳分数
if (device.getLastHeartbeatTime() != null) {
long minutesSinceHeartbeat = (System.currentTimeMillis() - device.getLastHeartbeatTime()) / (60 * 1000);
if (minutesSinceHeartbeat < 5) {
score += 5;
}
}
return Math.min(score, 100);
}
/**
* 构建推荐理由
*/
private String buildRecommendationReason(BadgeDeviceStatusDTO device, OrderDispatchContext context) {
StringBuilder reason = new StringBuilder();
reason.append("同区域工牌设备");
if (device.getStatus() != null) {
reason.append("、状态=").append(device.getStatus().getDescription());
}
if (device.getBatteryLevel() != null) {
reason.append("、电量").append(device.getBatteryLevel()).append("%");
}
if (device.getCurrentAreaName() != null) {
reason.append("、位置=").append(device.getCurrentAreaName());
}
if (context.isUrgent()) {
reason.append("、适合P0紧急任务");
}
return reason.toString();
}
// ==================== 兼容方法 ====================
/**
* 为新工单推荐设备(兼容旧接口)
* <p>
* 这是主要的推荐方法,用于在工单创建时选择合适的设备
*
* @param areaId 区域ID
* @param priority 工单优先级
* @return 推荐的设备ID如果没有合适的返回null
*/
public Long recommendDeviceForNewOrder(Long areaId, com.viewsh.module.ops.enums.PriorityEnum priority) {
log.info("为新工单推荐工牌设备: areaId={}, priority={}", areaId, priority);
// 使用新的策略接口方法
OrderDispatchContext context = OrderDispatchContext.builder()
.areaId(areaId)
.priority(priority)
.businessType(BUSINESS_TYPE)
.build();
AssigneeRecommendation recommendation = recommend(context);
if (recommendation != null && recommendation.hasRecommendation()) {
log.info("为新工单推荐工牌设备: areaId={}, priority={}, deviceId={}, deviceCode={}",
areaId, priority, recommendation.getAssigneeId(), recommendation.getAssigneeName());
return recommendation.getAssigneeId();
}
log.warn("未找到可用的工牌设备: areaId={}", areaId);
return null;
}
}

View File

@@ -0,0 +1,147 @@
package com.viewsh.module.ops.environment.service.dispatch;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.api.queue.OrderQueueDTO;
import com.viewsh.module.ops.api.queue.OrderQueueService;
import com.viewsh.module.ops.core.dispatch.DispatchEngine;
import com.viewsh.module.ops.core.dispatch.model.AssigneeStatus;
import com.viewsh.module.ops.core.dispatch.model.DispatchDecision;
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
import com.viewsh.module.ops.core.dispatch.strategy.InterruptDecision;
import com.viewsh.module.ops.core.dispatch.strategy.ScheduleStrategy;
import com.viewsh.module.ops.environment.integration.adapter.BadgeDeviceAssigneeStatusAdapter;
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 工牌设备优先级调度策略
* <p>
* 职责:怎么派单
* <p>
* 策略规则:
* <ul>
* <li>空闲无任务 → DIRECT_DISPATCH直接派单</li>
* <li>空闲有等待 → PUSH_AND_ENQUEUE推送等待+新任务入队)</li>
* <li>忙碌且非P0 → ENQUEUE_ONLY仅入队</li>
* <li>忙碌且P0 → INTERRUPT_AND_DISPATCH打断并派单</li>
* </ul>
*
* @author lzh
*/
@Component
@Slf4j
public class BadgeDeviceScheduleStrategy implements ScheduleStrategy {
private static final String STRATEGY_NAME = "badge_device_schedule";
private static final String BUSINESS_TYPE = "CLEAN";
@Resource
private BadgeDeviceStatusService badgeDeviceStatusService;
@Resource
private OrderQueueService orderQueueService;
@Resource
private DispatchEngine dispatchEngine;
@PostConstruct
public void init() {
dispatchEngine.registerScheduleStrategy(BUSINESS_TYPE, this);
log.info("工牌设备调度策略已注册: strategyName={}, businessType={}", STRATEGY_NAME, BUSINESS_TYPE);
}
@Override
public String getName() {
return STRATEGY_NAME;
}
@Override
public String getSupportedBusinessType() {
return BUSINESS_TYPE;
}
@Override
public DispatchDecision decide(OrderDispatchContext context) {
// assigneeId 存储的是设备ID
Long deviceId = context.getRecommendedAssigneeId();
if (deviceId == null) {
return DispatchDecision.unavailable("未指定设备");
}
// 查询设备状态
BadgeDeviceStatusDTO deviceStatus = badgeDeviceStatusService.getBadgeStatus(deviceId);
if (deviceStatus == null) {
return DispatchDecision.unavailable("设备不存在: " + deviceId);
}
// 转换为通用状态接口
AssigneeStatus assigneeStatus = new BadgeDeviceAssigneeStatusAdapter(deviceStatus);
context.setAssigneeStatus(assigneeStatus);
// 查询等待任务数量
List<OrderQueueDTO> waitingTasks = orderQueueService.getWaitingTasksByUserId(deviceId);
int waitingCount = waitingTasks != null ? waitingTasks.size() : 0;
log.info("工牌设备调度决策: deviceId={}, deviceCode={}, status={}, waitingCount={}, orderIsUrgent={}",
deviceId, deviceStatus.getDeviceCode(), assigneeStatus.getStatus(), waitingCount, context.isUrgent());
// 决策调度路径
if (assigneeStatus.isIdle() && assigneeStatus.getCurrentTaskCount() == 0) {
// 空闲且无正在执行的任务
if (waitingCount > 0) {
// 有等待任务,先推送等待任务
log.info("决策: PUSH_AND_ENQUEUE - 设备空闲但有等待任务");
return DispatchDecision.pushAndEnqueue();
} else {
// 直接派单
log.info("决策: DIRECT_DISPATCH - 设备空闲无等待任务");
return DispatchDecision.directDispatch();
}
} else if (context.isUrgent()) {
// P0紧急任务需要打断
if (assigneeStatus.getCurrentTaskCount() > 0) {
Long currentOrderId = deviceStatus.getCurrentOpsOrderId();
log.warn("决策: INTERRUPT_AND_DISPATCH - P0紧急任务打断当前任务: currentOrderId={}", currentOrderId);
return DispatchDecision.interruptAndDispatch(currentOrderId);
} else {
log.info("决策: DIRECT_DISPATCH - P0紧急任务直接派单");
return DispatchDecision.directDispatch();
}
} else {
// 非紧急任务,设备忙碌,入队等待
log.info("决策: ENQUEUE_ONLY - 设备忙碌,任务入队等待");
return DispatchDecision.enqueueOnly();
}
}
@Override
public InterruptDecision evaluateInterrupt(Long currentAssigneeId, Long currentOrderId,
OrderDispatchContext urgentContext) {
log.info("评估是否可打断工牌设备任务: deviceId={}, currentOrderId={}, urgentOrderId={}, urgentPriority={}",
currentAssigneeId, currentOrderId, urgentContext.getOrderId(), urgentContext.getPriority());
// 检查设备状态
BadgeDeviceStatusDTO deviceStatus = badgeDeviceStatusService.getBadgeStatus(currentAssigneeId);
if (deviceStatus == null) {
return InterruptDecision.deny("设备不存在", "无法执行打断");
}
// P0任务可以打断任何任务
if (urgentContext.isUrgent()) {
log.warn("允许打断: P0紧急任务可以打断当前任务");
return InterruptDecision.allowByDefault();
}
// P1/P2任务不能打断
log.info("拒绝打断: 非P0任务不能打断当前任务");
return InterruptDecision.deny(
"紧急任务优先级不足",
"建议等待当前任务完成"
);
}
}

View File

@@ -0,0 +1,123 @@
package com.viewsh.module.ops.environment.service.dispatch;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.core.dispatch.DispatchEngine;
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService;
import com.viewsh.module.ops.environment.test.BadgeDispatchTestConfig;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import jakarta.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
/**
* 工牌设备调度流程集成测试
*
* @author lzh
*/
@Slf4j
@SpringJUnitConfig(classes = BadgeDispatchTestConfig.class)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class})
public class BadgeDeviceDispatchTest {
@Resource
private BadgeDeviceStatusService badgeDeviceStatusService;
@Resource
private CleanOrderService cleanOrderService;
@Resource
private DispatchEngine dispatchEngine;
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 测试区域ID - A座2楼男卫
*/
private static final Long TEST_AREA_ID = 1301L;
/**
* 测试设备ID
*/
private static final Long TEST_DEVICE_ID = 31L;
private static final String TEST_DEVICE_CODE = "09207455611";
/**
* 第二个设备
*/
private static final Long TEST_DEVICE_2 = 34L;
private static final String TEST_DEVICE_2_CODE = "09207457042";
@BeforeEach
void setUp() {
log.info("========================================");
log.info("开始准备测试数据...");
log.info("========================================");
// 清理之前的测试数据
try {
badgeDeviceStatusService.deleteBadgeStatus(TEST_DEVICE_ID);
badgeDeviceStatusService.deleteBadgeStatus(TEST_DEVICE_2);
} catch (Exception e) {
log.warn("清理测试数据失败(可能首次运行): {}", e.getMessage());
}
// 初始化区域设备索引
try {
badgeDeviceStatusService.addToAreaIndex(TEST_DEVICE_ID, TEST_AREA_ID);
badgeDeviceStatusService.addToAreaIndex(TEST_DEVICE_2, TEST_AREA_ID);
} catch (Exception e) {
log.warn("初始化区域索引失败: {}", e.getMessage());
}
log.info("测试数据准备完成: areaId={}, deviceId={}", TEST_AREA_ID, TEST_DEVICE_ID);
}
/**
* 简单测试 - 只测试心跳和状态
*/
@Test
void testHeartbeatAndStatus() {
log.info("========================================");
log.info("测试:心跳和状态");
log.info("========================================");
try {
// 模拟心跳
badgeDeviceStatusService.handleHeartbeatWithArea(
TEST_DEVICE_ID,
TEST_DEVICE_CODE,
75,
TEST_AREA_ID,
"A座2楼男卫"
);
// 查询状态
BadgeDeviceStatusDTO status = badgeDeviceStatusService.getBadgeStatus(TEST_DEVICE_ID);
log.info("========================================");
log.info("心跳和状态测试完成");
log.info("设备状态: status={}, battery={}%, area={}",
status.getStatus(), status.getBatteryLevel(), status.getCurrentAreaName());
log.info("========================================");
assertNotNull(status, "设备状态不应为空");
assertEquals("idle", status.getStatus().getCode());
} catch (Exception e) {
log.error("心跳测试失败", e);
fail("测试失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,121 @@
package com.viewsh.module.ops.environment.test;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viewsh.module.ops.api.queue.OrderQueueDTO;
import com.viewsh.module.ops.api.queue.OrderQueueService;
import com.viewsh.module.ops.core.dispatch.DispatchEngine;
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusServiceImpl;
import com.viewsh.module.ops.environment.service.dispatch.BadgeDeviceAreaAssignStrategy;
import com.viewsh.module.ops.environment.service.dispatch.BadgeDeviceScheduleStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 测试配置类
* 使用 Mock 避免外部依赖Redis、数据库等
*
* @author lzh
*/
@Configuration
public class BadgeDispatchTestConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = mock(RedisTemplate.class);
when(template.opsForHash()).thenReturn(mock(org.springframework.data.redis.core.HashOperations.class));
when(template.opsForSet()).thenReturn(mock(org.springframework.data.redis.core.SetOperations.class));
when(template.keys(anyString())).thenReturn(new HashSet<>());
when(template.expire(anyString(), anyLong(), any())).thenReturn(true);
when(template.delete(anyString())).thenReturn(true);
return template;
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public BadgeDeviceStatusServiceImpl badgeDeviceStatusServiceImpl(
RedisTemplate<String, Object> redisTemplate,
ObjectMapper objectMapper) {
BadgeDeviceStatusServiceImpl service = new BadgeDeviceStatusServiceImpl();
setField(service, "redisTemplate", redisTemplate);
setField(service, "objectMapper", objectMapper);
try {
service.afterPropertiesSet();
} catch (Exception e) {
// ignore
}
return service;
}
@Bean
public BadgeDeviceStatusService badgeDeviceStatusService(BadgeDeviceStatusServiceImpl impl) {
return impl;
}
@Bean
public OrderQueueService orderQueueService() {
OrderQueueService mockService = mock(OrderQueueService.class);
when(mockService.getWaitingTasksByUserId(anyLong())).thenReturn(Collections.emptyList());
return mockService;
}
@Bean
public DispatchEngine dispatchEngine() {
return new DispatchEngine();
}
@Bean
public BadgeDeviceAreaAssignStrategy badgeDeviceAreaAssignStrategy(
BadgeDeviceStatusService badgeDeviceStatusService,
OrderQueueService orderQueueService,
DispatchEngine dispatchEngine) {
BadgeDeviceAreaAssignStrategy strategy = new BadgeDeviceAreaAssignStrategy();
setField(strategy, "badgeDeviceStatusService", badgeDeviceStatusService);
setField(strategy, "orderQueueService", orderQueueService);
setField(strategy, "dispatchEngine", dispatchEngine);
strategy.init();
return strategy;
}
@Bean
public BadgeDeviceScheduleStrategy badgeDeviceScheduleStrategy(
BadgeDeviceStatusService badgeDeviceStatusService,
OrderQueueService orderQueueService,
DispatchEngine dispatchEngine) {
BadgeDeviceScheduleStrategy strategy = new BadgeDeviceScheduleStrategy();
setField(strategy, "badgeDeviceStatusService", badgeDeviceStatusService);
setField(strategy, "orderQueueService", orderQueueService);
setField(strategy, "dispatchEngine", dispatchEngine);
strategy.init();
return strategy;
}
/**
* 通过反射设置私有字段
*/
private void setField(Object target, String fieldName, Object value) {
try {
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (Exception e) {
throw new RuntimeException("Failed to set field: " + fieldName, e);
}
}
}

View File

@@ -0,0 +1,25 @@
package com.viewsh.module.ops.environment.test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
/**
* 测试用 Spring Boot 应用配置
* 排除数据源自动配置,避免数据库连接问题
*
* @author lzh
*/
@SpringBootApplication(
scanBasePackages = {
"com.viewsh.module.ops.environment",
"com.viewsh.module.ops.core"
},
exclude = {
DataSourceAutoConfiguration.class,
RedisAutoConfiguration.class
}
)
public class TestApplication {
}

View File

@@ -0,0 +1,290 @@
-- ============================================
-- 区域设备关联完整配置(三个设备各司其职)
-- ============================================
-- 设备类型说明:
-- BADGE → buttonEvent按键映射配置
-- BEACON → beaconPresence信标MAC + 窗口配置)
-- TRAFFIC_COUNTER → trafficThreshold触发阈值配置
--
-- 已知设备信息:
-- 工牌31(09207455611), 34(09207457042) | 产品: 19 / AOQwO9pJWKgfFTk4
-- 客流32 | 产品: 21 / 82Zr08RUnstRHRO2
-- 信标需要补充设备ID
-- ============================================
-- ============================================
-- 清理旧数据(可选)
-- ============================================
-- DELETE FROM `ops_area_device_relation` WHERE `id` BETWEEN 10000 AND 10999;
-- ============================================
-- 1. A座2楼男卫 (area_id = 1301)
-- ============================================
-- 1.1 工牌设备 - 按键映射配置
INSERT INTO `ops_area_device_relation` (
`id`, `area_id`, `device_id`, `device_key`, `product_id`, `product_key`,
`relation_type`, `config_data`, `enabled`, `creator`, `create_time`,
`updater`, `update_time`, `deleted`, `tenant_id`
) VALUES (
10101, -- id
1301, -- area_id (A座2楼男卫)
31, -- device_id (工牌)
'09207455611', -- device_key
19, -- product_id
'AOQwO9pJWKgfFTk4', -- product_key
'BADGE', -- relation_type
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
}
}',
1, -- enabled
'system',
'2026-01-23 09:56:22',
'system',
'2026-01-23 09:56:22',
b'0',
1
);
-- 1.2 信标设备 - MAC地址 + 窗口配置
-- 注意device_id 需要替换为实际的信标设备ID
INSERT INTO `ops_area_device_relation` (
`id`, `area_id`, `device_id`, `device_key`, `product_id`, `product_key`,
`relation_type`, `config_data`, `enabled`, `creator`, `create_time`,
`updater`, `update_time`, `deleted`, `tenant_id`
) VALUES (
10102, -- id
1301, -- area_id (A座2楼男卫)
37, -- device_id (信标需替换为实际ID)
'BEACON_MALE_001', -- device_key
21, -- product_id (假设与客流同产品,需确认)
'82Zr08RUnstRHRO2', -- product_key
'BEACON', -- relation_type
'{
"beaconPresence": {
"enabled": true,
"beaconMac": "F0:C8:60:1D:10:BB",
"window": {
"sampleTtlSeconds": 120,
"missingValue": -999
},
"enter": {
"rssiThreshold": -70,
"windowSize": 3,
"hitCount": 2,
"autoArrival": true
},
"exit": {
"weakRssiThreshold": -85,
"windowSize": 5,
"hitCount": 4,
"warningDelayMinutes": 0,
"lossTimeoutMinutes": 10,
"minValidWorkMinutes": 3,
"autoComplete": true
}
}
}',
1, -- enabled
'system',
'2026-01-23 09:56:22',
'system',
'2026-01-23 09:56:22',
b'0',
1
);
-- 1.3 客流计数器 - 触发阈值配置
INSERT INTO `ops_area_device_relation` (
`id`, `area_id`, `device_id`, `device_key`, `product_id`, `product_key`,
`relation_type`, `config_data`, `enabled`, `creator`, `create_time`,
`updater`, `update_time`, `deleted`, `tenant_id`
) VALUES (
10103, -- id
1301, -- area_id (A座2楼男卫)
32, -- device_id (客流计数器)
'11225420037', -- device_key
21, -- product_id
'82Zr08RUnstRHRO2', -- product_key
'TRAFFIC_COUNTER', -- relation_type
'{
"trafficThreshold": {
"threshold": 100,
"timeWindowSeconds": 3600,
"autoCreateOrder": true,
"orderPriority": "P1"
}
}',
1, -- enabled
'system',
'2026-01-23 09:56:22',
'system',
'2026-01-23 09:56:22',
b'0',
1
);
-- ============================================
-- 2. A座2楼女卫 (area_id = 1302)
-- ============================================
-- 2.1 工牌设备 - 按键映射配置
INSERT INTO `ops_area_device_relation` (
`id`, `area_id`, `device_id`, `device_key`, `product_id`, `product_key`,
`relation_type`, `config_data`, `enabled`, `creator`, `create_time`,
`updater`, `update_time`, `deleted`, `tenant_id`
) VALUES (
10201, -- id
1302, -- area_id (A座2楼女卫)
34, -- device_id (工牌)
'09207457042', -- device_key
19, -- product_id
'AOQwO9pJWKgfFTk4', -- product_key
'BADGE', -- relation_type
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
}
}',
1, -- enabled
'system',
'2026-01-23 09:56:22',
'system',
'2026-01-23 09:56:22',
b'0',
1
);
-- 2.2 信标设备 - MAC地址 + 窗口配置
-- 注意device_id 需要替换为实际的信标设备ID
INSERT INTO `ops_area_device_relation` (
`id`, `area_id`, `device_id`, `device_key`, `product_id`, `product_key`,
`relation_type`, `config_data`, `enabled`, `creator`, `create_time`,
`updater`, `update_time`, `deleted`, `tenant_id`
) VALUES (
10202, -- id
1302, -- area_id (A座2楼女卫)
38, -- device_id (信标需替换为实际ID)
'BEACON_FEMALE_001', -- device_key
21, -- product_id
'82Zr08RUnstRHRO2', -- product_key
'BEACON', -- relation_type
'{
"beaconPresence": {
"enabled": true,
"beaconMac": "F0:C8:60:1D:10:BC",
"window": {
"sampleTtlSeconds": 120,
"missingValue": -999
},
"enter": {
"rssiThreshold": -70,
"windowSize": 3,
"hitCount": 2,
"autoArrival": true
},
"exit": {
"weakRssiThreshold": -85,
"windowSize": 5,
"hitCount": 4,
"warningDelayMinutes": 0,
"lossTimeoutMinutes": 10,
"minValidWorkMinutes": 3,
"autoComplete": true
}
}
}',
1, -- enabled
'system',
'2026-01-23 09:56:22',
'system',
'2026-01-23 09:56:22',
b'0',
1
);
-- 2.3 客流计数器 - 触发阈值配置
INSERT INTO `ops_area_device_relation` (
`id`, `area_id`, `device_id`, `device_key`, `product_id`, `product_key`,
`relation_type`, `config_data`, `enabled`, `creator`, `create_time`,
`updater`, `update_time`, `deleted`, `tenant_id`
) VALUES (
10203, -- id
1302, -- area_id (A座2楼女卫)
33, -- device_id (同一客流计数器)
'11225420051', -- device_key
21, -- product_id
'82Zr08RUnstRHRO2', -- product_key
'TRAFFIC_COUNTER', -- relation_type
'{
"trafficThreshold": {
"threshold": 80, -- 女卫阈值设低一些
"timeWindowSeconds": 3600,
"autoCreateOrder": true,
"orderPriority": "P1"
}
}',
1, -- enabled
'system',
'2026-01-23 09:56:22',
'system',
'2026-01-23 09:56:22',
b'0',
1
);
-- ============================================
-- 3. 验证查询
-- ============================================
-- 查看所有关联(按区域、类型分组)
SELECT
r.id,
r.area_id,
a.area_name,
r.relation_type,
r.device_id,
r.device_key,
r.enabled,
JSON_EXTRACT(r.config_data, '$.buttonEvent') AS button_event,
JSON_EXTRACT(r.config_data, '$.beaconPresence.beaconMac') AS beacon_mac,
JSON_EXTRACT(r.config_data, '$.trafficThreshold.threshold') AS traffic_threshold
FROM `ops_area_device_relation` r
LEFT JOIN `ops_bus_area` a ON r.area_id = a.id
WHERE r.id BETWEEN 10101 AND 10203
ORDER BY r.area_id, FIELD(r.relation_type, 'BADGE', 'BEACON', 'TRAFFIC_COUNTER');
-- 查看某个区域的所有设备
SELECT
r.relation_type,
r.device_id,
r.device_key,
r.config_data
FROM `ops_area_device_relation` r
WHERE r.area_id = 1301 -- 1301=男卫, 1302=女卫
AND r.enabled = 1
AND r.deleted = 0;
-- ============================================
-- 4. 查询你的设备信息确认信标设备ID
-- ============================================
-- 查看所有工牌和客流设备
SELECT id, device_name, nickname, serial_number, product_id, product_key, device_type, state
FROM iot_device
WHERE id IN (31, 32, 34)
ORDER BY id;
-- 查找信标类设备(根据产品或类型)
SELECT id, device_name, nickname, serial_number, product_id, product_key, device_type, state
FROM iot_device
WHERE device_type LIKE '%BEACON%'
OR product_key LIKE '%BEACON%'
OR device_name LIKE '%信标%'
ORDER BY id;

View File

@@ -0,0 +1,303 @@
-- ============================================
-- 工牌设备调度流程测试数据
-- ============================================
-- 说明:
-- 1. 先执行此 SQL 插入测试数据
-- 2. 运行 BadgeDeviceDispatchTest 测试类
-- 3. device_id 需要在 iot_device 表中存在,如不存在需先创建设备
-- ============================================
-- ============================================
-- 1. 区域测试数据 (ops_bus_area)
-- ============================================
-- 清理旧测试数据
DELETE FROM ops_bus_area WHERE id BETWEEN 1000 AND 1999;
-- 插入测试区域
INSERT INTO `ops_bus_area` (`id`, `parent_id`, `parent_path`, `area_name`, `area_code`, `area_type`, `function_type`, `floor_no`, `cleaning_frequency`, `standard_duration`, `area_level`, `is_active`, `sort`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES
-- 园区级别
(1000, NULL, '/1000', '测试科技园', 'TEST_PARK', 'PARK', NULL, NULL, 1, 30, 'MEDIUM', 1, 1, 'system', NOW(), 'system', NOW(), 0, 1),
-- 楼栋级别
(1100, 1000, '/1000/1100', 'A座写字楼', 'BLDG_A', 'BUILDING', NULL, NULL, 1, 30, 'MEDIUM', 1, 1, 'system', NOW(), 'system', NOW(), 0, 1),
(1101, 1000, '/1000/1101', 'B座写字楼', 'BLDG_B', 'BUILDING', NULL, NULL, 1, 30, 'MEDIUM', 1, 2, 'system', NOW(), 'system', NOW(), 0, 1),
-- 楼层级别 - A座
(1200, 1100, '/1000/1100/1200', 'A座1楼', 'A_F1', 'FLOOR', NULL, 1, 2, 30, 'MEDIUM', 1, 1, 'system', NOW(), 'system', NOW(), 0, 1),
(1201, 1100, '/1000/1100/1201', 'A座2楼', 'A_F2', 'FLOOR', NULL, 2, 2, 30, 'MEDIUM', 1, 2, 'system', NOW(), 'system', NOW(), 0, 1),
(1202, 1100, '/1000/1100/1202', 'A座3楼', 'A_F3', 'FLOOR', NULL, 3, 1, 20, 'LOW', 1, 3, 'system', NOW(), 'system', NOW(), 0, 1),
-- 楼层级别 - B座
(1203, 1101, '/1000/1101/1203', 'B座1楼', 'B_F1', 'FLOOR', NULL, 1, 2, 30, 'HIGH', 1, 1, 'system', NOW(), 'system', NOW(), 0, 1),
(1204, 1101, '/1000/1101/1204', 'B座2楼', 'B_F2', 'FLOOR', NULL, 2, 2, 30, 'MEDIUM', 1, 2, 'system', NOW(), 'system', NOW(), 0, 1),
-- 功能区域 - A座2楼主要测试区域
(1300, 1201, '/1000/1100/1201/1300', 'A座2楼电梯厅', 'A_F2_ELEVATOR', 'FUNCTION', 'ELEVATOR', 2, 4, 15, 'HIGH', 1, 1, 'system', NOW(), 'system', NOW(), 0, 1),
(1301, 1201, '/1000/1100/1201/1301', 'A座2楼男卫', 'A_F2_MALE_TOILET', 'FUNCTION', 'MALE_TOILET', 2, 4, 20, 'HIGH', 1, 2, 'system', NOW(), 'system', NOW(), 0, 1),
(1302, 1201, '/1000/1100/1201/1302', 'A座2楼女卫', 'A_F2_FEMALE_TOILET', 'FUNCTION', 'FEMALE_TOILET', 2, 4, 20, 'HIGH', 1, 3, 'system', NOW(), 'system', NOW(), 0, 1),
(1303, 1201, '/1000/1100/1201/1303', 'A座2楼走廊', 'A_F2_CORRIDOR', 'FUNCTION', 'PUBLIC', 2, 2, 30, 'MEDIUM', 1, 4, 'system', NOW(), 'system', NOW(), 0, 1),
(1304, 1201, '/1000/1100/1201/1304', 'A座2楼会议室', 'A_F2_MEETING', 'FUNCTION', 'PUBLIC', 2, 1, 15, 'LOW', 1, 5, 'system', NOW(), 'system', NOW(), 0, 1),
-- 功能区域 - B座1楼
(1305, 1203, '/1000/1101/1203/1305', 'B座1楼大堂', 'B_F1_LOBBY', 'FUNCTION', 'PUBLIC', 1, 6, 45, 'HIGH', 1, 1, 'system', NOW(), 'system', NOW(), 0, 1),
(1306, 1203, '/1000/1101/1203/1306', 'B座1楼电梯厅', 'B_F1_ELEVATOR', 'FUNCTION', 'ELEVATOR', 1, 4, 15, 'HIGH', 1, 2, 'system', NOW(), 'system', NOW(), 0, 1);
-- ============================================
-- 2. 区域设备关联测试数据 (ops_area_device_relation)
-- ============================================
-- config_data 按照 CleanOrderIntegrationConfig 文档配置
-- 清理旧测试数据
DELETE FROM ops_area_device_relation WHERE id BETWEEN 10000 AND 10999;
DELETE FROM ops_area_device_relation WHERE device_id IN (2011, 2012, 2013, 2014, 2015, 2021, 2022);
-- 假设产品ID根据实际情况调整或使用现有产品ID
SET @TEST_PRODUCT_ID = 1;
-- 插入测试设备关联
-- BADGE 类型设备配置buttonEvent按键事件+ beaconPresence信标检测
INSERT INTO `ops_area_device_relation` (`id`, `area_id`, `device_id`, `device_key`, `product_id`, `product_key`, `relation_type`, `config_data`, `enabled`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES
-- A座2楼电梯厅设备主要测试区域 - area_id=1300
(10000, 1300, 2011, 'BADGE_A2_E1', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 'BADGE',
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
},
"beaconPresence": {
"enabled": true,
"beaconMac": "F0:C8:60:1D:10:BB",
"window": {
"sampleTtlSeconds": 300,
"missingValue": -999
},
"enter": {
"rssiThreshold": -70,
"windowSize": 3,
"hitCount": 2,
"autoArrival": true
},
"exit": {
"weakRssiThreshold": -85,
"windowSize": 5,
"hitCount": 4,
"warningDelayMinutes": 0,
"lossTimeoutMinutes": 5,
"minValidWorkMinutes": 2,
"autoComplete": true
}
}
}', 1, 'system', NOW(), 'system', NOW(), 0, 1),
(10001, 1300, 2012, 'BADGE_A2_E2', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 'BADGE',
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
},
"beaconPresence": {
"enabled": true,
"beaconMac": "F0:C8:60:1D:10:BC",
"window": {
"sampleTtlSeconds": 300,
"missingValue": -999
},
"enter": {
"rssiThreshold": -70,
"windowSize": 3,
"hitCount": 2,
"autoArrival": true
},
"exit": {
"weakRssiThreshold": -85,
"windowSize": 5,
"hitCount": 4,
"warningDelayMinutes": 0,
"lossTimeoutMinutes": 5,
"minValidWorkMinutes": 2,
"autoComplete": true
}
}
}', 1, 'system', NOW(), 'system', NOW(), 0, 1),
(10002, 1300, 2013, 'BADGE_A2_E3', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 'BADGE',
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
},
"beaconPresence": {
"enabled": false,
"beaconMac": null,
"window": null,
"enter": null,
"exit": null
}
}', 1, 'system', NOW(), 'system', NOW(), 0, 1),
-- A座2楼其他功能区域设备
(10003, 1301, 2014, 'BADGE_A2_M1', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 'BADGE',
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
},
"beaconPresence": {
"enabled": true,
"beaconMac": "F0:C8:60:1D:10:BD",
"window": {
"sampleTtlSeconds": 300,
"missingValue": -999
},
"enter": {
"rssiThreshold": -70,
"windowSize": 3,
"hitCount": 2,
"autoArrival": true
},
"exit": {
"weakRssiThreshold": -85,
"windowSize": 5,
"hitCount": 4,
"warningDelayMinutes": 0,
"lossTimeoutMinutes": 5,
"minValidWorkMinutes": 2,
"autoComplete": true
}
}
}', 1, 'system', NOW(), 'system', NOW(), 0, 1),
(10004, 1302, 2015, 'BADGE_A2_F1', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 'BADGE',
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
},
"beaconPresence": {
"enabled": true,
"beaconMac": "F0:C8:60:1D:10:BE",
"window": {
"sampleTtlSeconds": 300,
"missingValue": -999
},
"enter": {
"rssiThreshold": -70,
"windowSize": 3,
"hitCount": 2,
"autoArrival": true
},
"exit": {
"weakRssiThreshold": -85,
"windowSize": 5,
"hitCount": 4,
"warningDelayMinutes": 0,
"lossTimeoutMinutes": 5,
"minValidWorkMinutes": 2,
"autoComplete": true
}
}
}', 1, 'system', NOW(), 'system', NOW(), 0, 1),
-- B座1楼设备用于跨区域测试
(10010, 1305, 2021, 'BADGE_B1_L1', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 'BADGE',
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
},
"beaconPresence": {
"enabled": false,
"beaconMac": null,
"window": null,
"enter": null,
"exit": null
}
}', 1, 'system', NOW(), 'system', NOW(), 0, 1),
(10011, 1306, 2022, 'BADGE_B1_E1', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 'BADGE',
'{
"buttonEvent": {
"enabled": true,
"confirmKeyId": 1,
"queryKeyId": 2
},
"beaconPresence": {
"enabled": true,
"beaconMac": "F0:C8:60:1D:10:BF",
"window": {
"sampleTtlSeconds": 300,
"missingValue": -999
},
"enter": {
"rssiThreshold": -70,
"windowSize": 3,
"hitCount": 2,
"autoArrival": true
},
"exit": {
"weakRssiThreshold": -85,
"windowSize": 5,
"hitCount": 4,
"warningDelayMinutes": 0,
"lossTimeoutMinutes": 5,
"minValidWorkMinutes": 2,
"autoComplete": true
}
}
}', 1, 'system', NOW(), 'system', NOW(), 0, 1);
-- ============================================
-- 3. IoT设备测试数据 (iot_device)
-- ============================================
-- 清理旧测试设备
DELETE FROM iot_device WHERE id BETWEEN 2011 AND 2030;
-- 创建测试工牌设备
INSERT INTO `iot_device` (`id`, `device_name`, `nickname`, `serial_number`, `product_id`, `product_key`, `device_type`, `state`, `active_time`, `tenant_id`) VALUES
(2011, '测试工牌_A2_E1', 'A座2楼工牌1', 'SN2011001', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 10, 3, NOW(), 1),
(2012, '测试工牌_A2_E2', 'A座2楼工牌2', 'SN2011002', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 10, 3, NOW(), 1),
(2013, '测试工牌_A2_E3', 'A座2楼工牌3', 'SN2011003', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 10, 3, NOW(), 1),
(2014, '测试工牌_A2_M1', 'A座2楼男卫工牌', 'SN2011004', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 10, 3, NOW(), 1),
(2015, '测试工牌_A2_F1', 'A座2楼女卫工牌', 'SN2011005', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 10, 3, NOW(), 1),
(2021, '测试工牌_B1_L1', 'B座1楼大堂工牌', 'SN2011006', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 10, 3, NOW(), 1),
(2022, '测试工牌_B1_E1', 'B座1楼电梯工牌', 'SN2011007', @TEST_PRODUCT_ID, 'BADGE_PRODUCT', 10, 3, NOW(), 1);
-- ============================================
-- 4. 验证查询
-- ============================================
-- 查看插入的区域
SELECT id, parent_id, parent_path, area_name, area_code, area_type, function_type, floor_no
FROM ops_bus_area
WHERE id BETWEEN 1000 AND 1999
ORDER BY parent_path, id;
-- 查看插入的设备关联(含配置)
SELECT r.id, r.area_id, a.area_name, r.device_id, r.device_key, r.relation_type, r.config_data
FROM ops_area_device_relation r
LEFT JOIN ops_bus_area a ON r.area_id = a.id
WHERE r.id BETWEEN 10000 AND 10999
ORDER BY r.area_id, r.id;
-- 查看插入的IoT设备
SELECT id, device_name, nickname, serial_number, state
FROM iot_device
WHERE id BETWEEN 2011 AND 2030
ORDER BY id;
-- 查看某区域下的所有工牌设备(含配置)
SELECT r.device_id, r.device_key, d.device_name, r.config_data
FROM ops_area_device_relation r
LEFT JOIN iot_device d ON r.device_id = d.id
WHERE r.area_id = 1300 -- A座2楼电梯厅
AND r.relation_type = 'BADGE'
AND r.enabled = 1
AND r.deleted = 0;