diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigService.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigService.java index 5a42f97..760bf10 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigService.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigService.java @@ -54,6 +54,19 @@ public interface CleanOrderIntegrationConfigService { */ java.util.List getConfigsByAreaIdAndRelationType(Long areaId, String relationType); + /** + * 根据区域ID和关联类型查询单个配置 + *

+ * 用于跨设备获取配置的场景,例如:工牌设备需要获取该区域的信标配置 + *

+ * 注意:如果同一区域同一类型有多个设备配置,返回第一个 + * + * @param areaId 区域ID + * @param relationType 关联类型(TRAFFIC_COUNTER/BEACON/BADGE) + * @return 集成配置包装器,如果不存在返回 null + */ + AreaDeviceConfigWrapper getConfigByAreaIdAndRelationType(Long areaId, String relationType); + /** * 清除设备配置缓存 *

diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java index 1dde87c..4227203 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java @@ -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 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); diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/adapter/BadgeDeviceAssigneeStatusAdapter.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/adapter/BadgeDeviceAssigneeStatusAdapter.java new file mode 100644 index 0000000..5554a90 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/adapter/BadgeDeviceAssigneeStatusAdapter.java @@ -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; + +/** + * 工牌设备状态适配器 + *

+ * 将 {@link BadgeDeviceStatusDTO} 适配为通用的 {@link AssigneeStatus} 接口 + *

+ * 设计说明: + * - 适配器模式:将设备状态对象转换为通用接口,供调度引擎使用 + * - 设备作为执行人: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; + } +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceAreaAssignStrategy.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceAreaAssignStrategy.java new file mode 100644 index 0000000..c930ca8 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceAreaAssignStrategy.java @@ -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; + +/** + * 工牌设备区域优先分配策略 + *

+ * 职责:哪个设备来接单 + *

+ * 策略规则: + * 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 devices = badgeDeviceStatusService.listBadgesByArea(context.getAreaId()); + if (devices.isEmpty()) { + log.warn("该区域没有工牌设备: areaId={}", context.getAreaId()); + return AssigneeRecommendation.none(); + } + + // 过滤在线的设备 + List 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 recommendBatch(OrderDispatchContext context, int limit) { + log.info("批量推荐工牌设备: areaId={}, priority={}, limit={}", + context.getAreaId(), context.getPriority(), limit); + + List 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 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 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(); + } + + // ==================== 兼容方法 ==================== + + /** + * 为新工单推荐设备(兼容旧接口) + *

+ * 这是主要的推荐方法,用于在工单创建时选择合适的设备 + * + * @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; + } +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceScheduleStrategy.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceScheduleStrategy.java new file mode 100644 index 0000000..0dec654 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceScheduleStrategy.java @@ -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; + +/** + * 工牌设备优先级调度策略 + *

+ * 职责:怎么派单 + *

+ * 策略规则: + *

+ * + * @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 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( + "紧急任务优先级不足", + "建议等待当前任务完成" + ); + } +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceDispatchTest.java b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceDispatchTest.java new file mode 100644 index 0000000..de8c33c --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceDispatchTest.java @@ -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 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()); + } + } +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/BadgeDispatchTestConfig.java b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/BadgeDispatchTestConfig.java new file mode 100644 index 0000000..f519a3f --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/BadgeDispatchTestConfig.java @@ -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 redisTemplate() { + RedisTemplate 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 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); + } + } +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/TestApplication.java b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/TestApplication.java new file mode 100644 index 0000000..9507803 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/TestApplication.java @@ -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 { + +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/resources/sql/area_device_relation_full_config.sql b/viewsh-module-ops/viewsh-module-environment-biz/src/test/resources/sql/area_device_relation_full_config.sql new file mode 100644 index 0000000..134fbd0 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/resources/sql/area_device_relation_full_config.sql @@ -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; diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/resources/sql/badge_device_dispatch_test_data.sql b/viewsh-module-ops/viewsh-module-environment-biz/src/test/resources/sql/badge_device_dispatch_test_data.sql new file mode 100644 index 0000000..c90e0ad --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/resources/sql/badge_device_dispatch_test_data.sql @@ -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;