feat(iot): 支持按区域和设备类型查询配置
新增 getConfigByAreaIdAndRelationType 方法,用于跨设备获取配置场景。 例如:工牌设备需要获取该区域的信标配置。 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
"紧急任务优先级不足",
|
||||
"建议等待当前任务完成"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user