diff --git a/viewsh-module-iot/viewsh-module-iot-gateway/src/main/resources/application.yaml b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/resources/application.yaml index 2e1513b..b09f713 100644 --- a/viewsh-module-iot/viewsh-module-iot-gateway/src/main/resources/application.yaml +++ b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/resources/application.yaml @@ -38,7 +38,7 @@ viewsh: gateway: # 设备 RPC 配置 rpc: - url: ${VIEWSH_IOT_GATEWAY_RPC_URL:http://127.0.0.1:48080} # 主程序 API 地址 + url: ${VIEWSH_IOT_GATEWAY_RPC_URL:http://127.0.0.1:48091} # 主程序 API 地址 connect-timeout: 30s read-timeout: 30s # 设备 Token 配置 diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/BadgeDeviceStatusRedisDAO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/BadgeDeviceStatusRedisDAO.java new file mode 100644 index 0000000..4715c02 --- /dev/null +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/BadgeDeviceStatusRedisDAO.java @@ -0,0 +1,152 @@ +package com.viewsh.module.iot.dal.redis.clean; + +import jakarta.annotation.Resource; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * 工牌设备状态 Redis DAO(只读) + *

+ * 从 Redis 读取由 Ops 模块写入的工牌设备状态信息 + * Key: ops:badge:status:{deviceId} + * 数据结构: Hash + *

+ * IoT 模块只读取此数据,��操作由 Ops 模块的 BadgeDeviceStatusService 处理 + * + * @author AI + */ +@Repository +public class BadgeDeviceStatusRedisDAO { + + /** + * 工牌状态 Key 模式 + *

+ * 格式:ops:badge:status:{deviceId} + */ + private static final String BADGE_STATUS_KEY_PATTERN = "ops:badge:status:%s"; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 获取设备当前工单信息 + * + * @param deviceId 设备ID + * @return 工单信息,如果不存在返回 null + */ + public OrderInfo getCurrentOrder(Long deviceId) { + String key = formatKey(deviceId); + Map map = stringRedisTemplate.opsForHash().entries(key); + + if (map == null || map.isEmpty()) { + return null; + } + + // 提取工单相关字段 + OrderInfo orderInfo = new OrderInfo(); + orderInfo.setOrderId(getLong(map.get("currentOpsOrderId"))); + orderInfo.setStatus((String) map.get("currentOrderStatus")); + orderInfo.setAreaId(getLong(map.get("currentAreaId"))); + orderInfo.setBeaconMac((String) map.get("beaconMac")); + + // 如果没有工单ID,返回 null + if (orderInfo.getOrderId() == null) { + return null; + } + + return orderInfo; + } + + /** + * 获取设备完整状态信息(原始 Map) + * + * @param deviceId 设备ID + * @return 状态信息 Map,如果不存在返回空 Map + */ + public Map getBadgeStatus(Long deviceId) { + String key = formatKey(deviceId); + Map map = stringRedisTemplate.opsForHash().entries(key); + + if (map == null || map.isEmpty()) { + return new HashMap<>(); + } + + Map result = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + result.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue())); + } + return result; + } + + /** + * 检查设备是否有当前工单 + * + * @param deviceId 设备ID + * @return 是否有工单 + */ + public boolean hasCurrentOrder(Long deviceId) { + String key = formatKey(deviceId); + return Boolean.TRUE.equals(stringRedisTemplate.opsForHash().hasKey(key, "currentOpsOrderId")); + } + + /** + * 获取 Hash 中的 Long 字段值 + */ + private Long getLong(Object value) { + if (value == null) { + return null; + } + if (value instanceof Long) { + return (Long) value; + } + try { + return Long.parseLong(value.toString()); + } catch (Exception e) { + return null; + } + } + + /** + * 格式化 Redis Key + */ + private static String formatKey(Long deviceId) { + return String.format(BADGE_STATUS_KEY_PATTERN, deviceId); + } + + /** + * 工单信息(精简 DTO) + *

+ * 只包含 IoT 模块需要的工单字段 + */ + @Getter + @Setter + public static class OrderInfo { + /** + * 工单ID + */ + private Long orderId; + + /** + * 工单状态 + *

+ * DISPATCHED/ARRIVED/PAUSED/COMPLETED + */ + private String status; + + /** + * 区域ID + */ + private Long areaId; + + /** + * 信标 MAC 地址 + */ + private String beaconMac; + } +} diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/DeviceCurrentOrderRedisDAO.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/DeviceCurrentOrderRedisDAO.java deleted file mode 100644 index f0f847b..0000000 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/redis/clean/DeviceCurrentOrderRedisDAO.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.viewsh.module.iot.dal.redis.clean; - -import com.viewsh.framework.common.util.json.JsonUtils; -import jakarta.annotation.Resource; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Repository; - -import java.util.concurrent.TimeUnit; - -/** - * 设备当前工单缓存 Redis DAO - *

- * 用于缓存设备当前执行的工单信息(由 Ops 下发) - * 减少物联网模块查询数据库的频率 - * - * @author AI - */ -@Repository -public class DeviceCurrentOrderRedisDAO { - - /** - * 工单缓存 Key 模式 - *

- * 格式:ops:clean:device:order:{deviceId} - */ - private static final String ORDER_KEY_PATTERN = "ops:clean:device:order:%s"; - - /** - * 工单缓存的 TTL(秒) - *

- * 默认保留 1 小时 - */ - private static final int ORDER_TTL_SECONDS = 3600; - - @Resource - private StringRedisTemplate stringRedisTemplate; - - /** - * 缓存设备当前工单 - * - * @param deviceId 设备ID - * @param orderInfo 工单缓存信息 - */ - public void cacheCurrentOrder(Long deviceId, OrderCacheInfo orderInfo) { - String key = formatKey(deviceId); - String json = JsonUtils.toJsonString(orderInfo); - stringRedisTemplate.opsForValue().set(key, json, ORDER_TTL_SECONDS, TimeUnit.SECONDS); - } - - /** - * 获取当前工单 - * - * @param deviceId 设备ID - * @return 工单缓存信息,如果不存在返回 null - */ - public OrderCacheInfo getCurrentOrder(Long deviceId) { - String key = formatKey(deviceId); - String json = stringRedisTemplate.opsForValue().get(key); - - if (json == null) { - return null; - } - - return JsonUtils.parseObject(json, OrderCacheInfo.class); - } - - /** - * 清除当前工单缓存 - * - * @param deviceId 设备ID - */ - public void clearCurrentOrder(Long deviceId) { - String key = formatKey(deviceId); - stringRedisTemplate.delete(key); - } - - /** - * 缓存被暂停的工单ID - *

- * 用于 P0 插队场景,恢复被暂停的工单 - * - * @param deviceId 设备ID - * @param pausedOrderId 被暂停的工单ID - */ - public void cachePausedOrder(Long deviceId, Long pausedOrderId) { - String key = formatKey(deviceId) + ":paused"; - stringRedisTemplate.opsForValue().set(key, String.valueOf(pausedOrderId), - ORDER_TTL_SECONDS, TimeUnit.SECONDS); - } - - /** - * 获取被暂停的工单ID - * - * @param deviceId 设备ID - * @return 被暂停的工单ID,如果不存在返回 null - */ - public Long getPausedOrderId(Long deviceId) { - String key = formatKey(deviceId) + ":paused"; - String orderIdStr = stringRedisTemplate.opsForValue().get(key); - return orderIdStr != null ? Long.parseLong(orderIdStr) : null; - } - - /** - * 清除被暂停的工单缓存 - * - * @param deviceId 设备ID - */ - public void clearPausedOrder(Long deviceId) { - String key = formatKey(deviceId) + ":paused"; - stringRedisTemplate.delete(key); - } - - /** - * 格式化 Redis Key - */ - private static String formatKey(Long deviceId) { - return String.format(ORDER_KEY_PATTERN, deviceId); - } - - /** - * 工单缓存信息 - */ - public static class OrderCacheInfo { - /** - * 工单ID - */ - private Long orderId; - - /** - * 工单状态 - *

- * DISPATCHED/ARRIVED/PAUSED/COMPLETED - */ - private String status; - - /** - * 区域ID - */ - private Long areaId; - - /** - * 信标 MAC 地址 - */ - private String beaconMac; - - public Long getOrderId() { - return orderId; - } - - public void setOrderId(Long orderId) { - this.orderId = orderId; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - - public Long getAreaId() { - return areaId; - } - - public void setAreaId(Long areaId) { - this.areaId = areaId; - } - - public String getBeaconMac() { - return beaconMac; - } - - public void setBeaconMac(String beaconMac) { - this.beaconMac = beaconMac; - } - } -} diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/mq/consumer/rule/IotCleanRuleMessageHandler.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/mq/consumer/rule/IotCleanRuleMessageHandler.java new file mode 100644 index 0000000..c747bba --- /dev/null +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/mq/consumer/rule/IotCleanRuleMessageHandler.java @@ -0,0 +1,59 @@ +package com.viewsh.module.iot.mq.consumer.rule; + +import com.viewsh.module.iot.core.messagebus.core.IotMessageBus; +import com.viewsh.module.iot.core.messagebus.core.IotMessageSubscriber; +import com.viewsh.module.iot.core.mq.message.IotDeviceMessage; +import com.viewsh.module.iot.service.rule.clean.CleanRuleProcessorManager; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 保洁规则消息处理器 + *

+ * 订阅设备消息总线,独立处理保洁相关规则,与属性保存解耦 + * + * 优化点: + * 1. 独立消费组,不阻塞属性保存主流程 + * 2. 与场景规则引擎架构一致,便于维护 + * 3. 可独立扩展消费者数量 + * + * @author AI + */ +@Component +@Slf4j +public class IotCleanRuleMessageHandler implements IotMessageSubscriber { + + @Resource + private CleanRuleProcessorManager cleanRuleProcessorManager; + + @Resource + private IotMessageBus messageBus; + + @PostConstruct + public void init() { + messageBus.register(this); + } + + @Override + public String getTopic() { + return IotDeviceMessage.MESSAGE_BUS_DEVICE_MESSAGE_TOPIC; + } + + @Override + public String getGroup() { + return "iot_clean_rule_consumer"; // 独立消费组 + } + + @Override + public void onMessage(IotDeviceMessage message) { + try { + cleanRuleProcessorManager.processMessage(message); + } catch (Exception e) { + // 规则处理异常不影响其他消息处理 + log.error("[onMessage][消息({}) 保洁规则处理异常]", message.getRequestId(), e); + } + } + +} diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/device/property/IotDevicePropertyServiceImpl.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/device/property/IotDevicePropertyServiceImpl.java index 9dce672..7775fbf 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/device/property/IotDevicePropertyServiceImpl.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/device/property/IotDevicePropertyServiceImpl.java @@ -85,14 +85,8 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { @Resource private IntegrationEventPublisher integrationEventPublisher; - @Resource - private com.viewsh.module.iot.service.rule.clean.processor.TrafficThresholdRuleProcessor trafficThresholdRuleProcessor; - - @Resource - private com.viewsh.module.iot.service.rule.clean.processor.BeaconDetectionRuleProcessor beaconDetectionRuleProcessor; - - @Resource - private com.viewsh.module.iot.service.rule.clean.processor.ButtonEventRuleProcessor buttonEventRuleProcessor; + // 注意:保洁规则处理器已迁移到独立的消息订阅者 IotCleanRuleMessageHandler + // 不再在此同步调用,避免阻塞属性保存主流程 // ========== 设备属性相关操作 ========== @@ -180,45 +174,14 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build()); deviceDataRedisDAO.putAll(device.getId(), properties2); - // 2.3 调用规则处理器(保洁工单集成) - processRuleProcessors(device, properties); + // 注意:保洁规则处理已迁移到独立消息订阅者 IotCleanRuleMessageHandler + // 通过订阅同一消息总线的 Topic 异步处理,不再阻塞属性保存主流程 - // 2.4 发布属性消息到 Redis Stream(供其他模块如 Ops 订阅) + // 2.3 发布属性消息到 Redis Stream(供其他模块如 Ops 订阅) // TODO: 暂停发布,后续根据需要开启 // publishPropertyMessage(device, properties, message.getReportTime()); } - /** - * 处理规则处理器(保洁工单集成) - *

- * 在设备属性上报处理流程中调用,检测是否满足工单创建/到岗/完成条件 - * - * @param device 设备信息 - * @param properties 属性数据 - */ - private void processRuleProcessors(IotDeviceDO device, Map properties) { - try { - // 遍历所有属性,调用规则处理器 - for (Map.Entry entry : properties.entrySet()) { - String identifier = entry.getKey(); - Object value = entry.getValue(); - - // 调用客流阈值规则处理器 - trafficThresholdRuleProcessor.processPropertyChange(device.getId(), identifier, value); - - // 调用蓝牙信标检测规则处理器 - beaconDetectionRuleProcessor.processPropertyChange(device.getId(), identifier, value); - - // 调用按键事件规则处理器 - buttonEventRuleProcessor.processPropertyChange(device.getId(), identifier, value); - } - } catch (Exception e) { - // 规则处理器异常不应阻塞属性上报主流程 - log.error("[processRuleProcessors] 规则处理器调用失败: deviceId={}, properties={}", - device.getId(), properties.keySet(), e); - } - } - /** * 发布设备属性消息 *

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 760bf10..51007de 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 @@ -12,17 +12,6 @@ import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrat */ public interface CleanOrderIntegrationConfigService { - /** - * 根据设备ID查询配置(带缓存) - *

- * 优先从 Redis 缓存读取,缓存未命中时从数据库查询并写入缓存 - * 缓存 TTL: 5 分钟 - * - * @param deviceId 设备ID - * @return 集成配置,如果不存在或未启用返回 null - */ - CleanOrderIntegrationConfig getConfigByDeviceId(Long deviceId); - /** * 根据设备ID查询配置包装器(包含完整信息) *

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 4227203..d9caf36 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 @@ -1,7 +1,6 @@ package com.viewsh.module.iot.service.integration.clean; import com.viewsh.framework.common.util.json.JsonUtils; -import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig; import com.viewsh.module.iot.dal.dataobject.integration.clean.OpsAreaDeviceRelationDO; import com.viewsh.module.iot.dal.mysql.integration.clean.OpsAreaDeviceRelationMapper; import jakarta.annotation.Resource; @@ -25,8 +24,8 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra /** * 配置缓存 Key 模式 */ - private static final String CONFIG_DEVICE_KEY_PATTERN = "iot:clean:config:device:%s"; - private static final String CONFIG_AREA_KEY_PATTERN = "iot:clean:config:area:%s"; + private static final String CONFIG_WRAPPER_KEY_PATTERN = "iot:clean:config:wrapper:%s"; + private static final String CONFIG_AREA_TYPE_KEY_PATTERN = "iot:clean:config:area:%s:type:%s"; /** * 配置缓存 TTL(秒) @@ -34,6 +33,11 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra * 5 分钟自动过期 */ private static final int CONFIG_CACHE_TTL_SECONDS = 300; + + /** + * 空值缓存标记(防止缓存穿透) + */ + private static final String NULL_CACHE_VALUE = "NULL"; @Resource private OpsAreaDeviceRelationMapper relationMapper; @@ -41,49 +45,6 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra @Resource private StringRedisTemplate stringRedisTemplate; - @Override - public CleanOrderIntegrationConfig getConfigByDeviceId(Long deviceId) { - log.debug("[CleanOrderConfig] 查询设备配置:deviceId={}", deviceId); - - // 1. 尝试从 Redis 缓存读取 - String cacheKey = formatDeviceKey(deviceId); - String cachedConfig = stringRedisTemplate.opsForValue().get(cacheKey); - - if (cachedConfig != null) { - log.debug("[CleanOrderConfig] 命中缓存:deviceId={}", deviceId); - return JsonUtils.parseObject(cachedConfig, CleanOrderIntegrationConfig.class); - } - - // 2. 从数据库查询 - OpsAreaDeviceRelationDO relation = relationMapper.selectByDeviceId(deviceId); - - if (relation == null) { - log.debug("[CleanOrderConfig] 设备无关联配置:deviceId={}", deviceId); - return null; - } - - if (!relation.getEnabled()) { - log.debug("[CleanOrderConfig] 设备配置已禁用:deviceId={}", deviceId); - return null; - } - - CleanOrderIntegrationConfig config = relation.getConfigData(); - - // 3. 写入缓存 - if (config != null) { - stringRedisTemplate.opsForValue().set( - cacheKey, - JsonUtils.toJsonString(config), - CONFIG_CACHE_TTL_SECONDS, - TimeUnit.SECONDS - ); - } - - log.debug("[CleanOrderConfig] 查询到配置:deviceId={}, areaId={}", deviceId, relation.getAreaId()); - - return config; - } - @Override public List getConfigsByAreaId(Long areaId) { log.debug("[CleanOrderConfig] 查询区域配置:areaId={}", areaId); @@ -111,44 +72,103 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra public AreaDeviceConfigWrapper getConfigByAreaIdAndRelationType(Long areaId, String relationType) { log.debug("[CleanOrderConfig] 查询单个区域配置:areaId={}, relationType={}", areaId, relationType); + // 1. 尝试从缓存读取 + String cacheKey = formatAreaTypeKey(areaId, relationType); + String cachedValue = stringRedisTemplate.opsForValue().get(cacheKey); + + if (cachedValue != null) { + if (NULL_CACHE_VALUE.equals(cachedValue)) { + log.debug("[CleanOrderConfig] 命中空值缓存:areaId={}, relationType={}", areaId, relationType); + return null; + } + log.debug("[CleanOrderConfig] 命中区域类型缓存:areaId={}, relationType={}", areaId, relationType); + return JsonUtils.parseObject(cachedValue, AreaDeviceConfigWrapper.class); + } + + // 2. 从数据库查询 List relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType); if (relations.isEmpty()) { + // 缓存空值,防止缓存穿透 + stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE_VALUE, 60, TimeUnit.SECONDS); return null; } // 返回第一个启用的配置 - return relations.stream() + AreaDeviceConfigWrapper wrapper = relations.stream() .filter(r -> r.getEnabled()) .findFirst() .map(this::wrapConfig) .orElse(null); + + // 3. 写入缓存 + if (wrapper != null) { + stringRedisTemplate.opsForValue().set( + cacheKey, + JsonUtils.toJsonString(wrapper), + CONFIG_CACHE_TTL_SECONDS, + TimeUnit.SECONDS + ); + } else { + // 缓存空值 + stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE_VALUE, 60, TimeUnit.SECONDS); + } + + return wrapper; } @Override public AreaDeviceConfigWrapper getConfigWrapperByDeviceId(Long deviceId) { log.debug("[CleanOrderConfig] 查询设备完整配置:deviceId={}", deviceId); + // 1. 尝试从缓存读取 + String cacheKey = formatWrapperKey(deviceId); + String cachedValue = stringRedisTemplate.opsForValue().get(cacheKey); + + if (cachedValue != null) { + if (NULL_CACHE_VALUE.equals(cachedValue)) { + log.debug("[CleanOrderConfig] 命中空值缓存:deviceId={}", deviceId); + return null; + } + log.debug("[CleanOrderConfig] 命中 Wrapper 缓存:deviceId={}", deviceId); + return JsonUtils.parseObject(cachedValue, AreaDeviceConfigWrapper.class); + } + + // 2. 从数据库查询 OpsAreaDeviceRelationDO relation = relationMapper.selectByDeviceId(deviceId); if (relation == null || !relation.getEnabled()) { + // 缓存空值,防止缓存穿透(TTL 较短:60秒) + stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE_VALUE, 60, TimeUnit.SECONDS); return null; } - return wrapConfig(relation); + AreaDeviceConfigWrapper wrapper = wrapConfig(relation); + + // 3. 写入缓存 + stringRedisTemplate.opsForValue().set( + cacheKey, + JsonUtils.toJsonString(wrapper), + CONFIG_CACHE_TTL_SECONDS, + TimeUnit.SECONDS + ); + + return wrapper; } @Override public void evictCache(Long deviceId) { - String cacheKey = formatDeviceKey(deviceId); - stringRedisTemplate.delete(cacheKey); + // 清除设备配置包装器缓存 + stringRedisTemplate.delete(formatWrapperKey(deviceId)); log.info("[CleanOrderConfig] 清除设备配置缓存:deviceId={}", deviceId); } @Override public void evictAreaCache(Long areaId) { - String cacheKey = formatAreaKey(areaId); - stringRedisTemplate.delete(cacheKey); + // 清除区域+类型配置缓存 + stringRedisTemplate.delete(formatAreaTypeKey(areaId, "TRAFFIC_COUNTER")); + stringRedisTemplate.delete(formatAreaTypeKey(areaId, "BEACON")); + stringRedisTemplate.delete(formatAreaTypeKey(areaId, "BADGE")); log.info("[CleanOrderConfig] 清除区域配置缓存:areaId={}", areaId); } @@ -168,16 +188,16 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra } /** - * 格式化设备配置缓存 Key + * 格式化设备配置包装器缓存 Key */ - private static String formatDeviceKey(Long deviceId) { - return String.format(CONFIG_DEVICE_KEY_PATTERN, deviceId); + private static String formatWrapperKey(Long deviceId) { + return String.format(CONFIG_WRAPPER_KEY_PATTERN, deviceId); } /** - * 格式化区域配置缓存 Key + * 格式化区域+类型配置缓存 Key */ - private static String formatAreaKey(Long areaId) { - return String.format(CONFIG_AREA_KEY_PATTERN, areaId); + private static String formatAreaTypeKey(Long areaId, String relationType) { + return String.format(CONFIG_AREA_TYPE_KEY_PATTERN, areaId, relationType); } } diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/CleanRuleProcessorManager.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/CleanRuleProcessorManager.java new file mode 100644 index 0000000..405f989 --- /dev/null +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/CleanRuleProcessorManager.java @@ -0,0 +1,100 @@ +package com.viewsh.module.iot.service.rule.clean; + +import com.viewsh.module.iot.core.enums.IotDeviceMessageMethodEnum; +import com.viewsh.module.iot.core.mq.message.IotDeviceMessage; +import com.viewsh.module.iot.service.rule.clean.processor.BeaconDetectionRuleProcessor; +import com.viewsh.module.iot.service.rule.clean.processor.ButtonEventRuleProcessor; +import com.viewsh.module.iot.service.rule.clean.processor.TrafficThresholdRuleProcessor; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 保洁规则处理器管理器 + *

+ * 统一管理所有保洁规则处理器,提供消息分发能力 + *

+ * 与场景规则处理器架构保持一致,直接在消息线程中同步处理 + * + * @author AI + */ +@Component +@Slf4j +public class CleanRuleProcessorManager { + + @Resource + private TrafficThresholdRuleProcessor trafficThresholdRuleProcessor; + + @Resource + private BeaconDetectionRuleProcessor beaconDetectionRuleProcessor; + + @Resource + private ButtonEventRuleProcessor buttonEventRuleProcessor; + + /** + * 处理设备消息 + *

+ * 处理属性上报和事件上报消息,其他消息类型忽略 + * + * @param message 设备消息 + */ + public void processMessage(IotDeviceMessage message) { + String method = message.getMethod(); + + // 1. 只处理属性上报和事件上报消息 + if (!IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod().equals(method) + && !IotDeviceMessageMethodEnum.EVENT_POST.getMethod().equals(method)) { + return; + } + + // 2. 获取事件/属性数据 + if (!(message.getParams() instanceof Map)) { + log.warn("[processMessage][消息({}) params 格式不正确]", message.getRequestId()); + return; + } + + Long deviceId = message.getDeviceId(); + @SuppressWarnings("unchecked") + Map data = (Map) message.getParams(); + + log.debug("[processMessage][设备({}) 处理{}上报,数据数量: {}]", + deviceId, "thing.event.post".equals(method) ? "事件" : "属性", data.size()); + + // 3. 顺序处理各数据项(与场景规则处理器保持一致) + data.forEach((identifier, value) -> + processDataSafely(deviceId, identifier, value)); + } + + /** + * 安全处理单个数据项 + *

+ * 按标识符路由到对应处理器: + * - 属性:people_in → TrafficThresholdRuleProcessor + * - 属性:bluetoothDevices → BeaconDetectionRuleProcessor + * - 事件:button_event → ButtonEventRuleProcessor + * + * @param deviceId 设备ID + * @param identifier 标识符 + * @param value 数据值 + */ + private void processDataSafely(Long deviceId, String identifier, Object value) { + try { + switch (identifier) { + case "people_in" -> + trafficThresholdRuleProcessor.processPropertyChange(deviceId, identifier, value); + case "bluetoothDevices" -> + beaconDetectionRuleProcessor.processPropertyChange(deviceId, identifier, value); + case "button_event" -> + buttonEventRuleProcessor.processPropertyChange(deviceId, identifier, value); + default -> { + // 其他属性/事件忽略 + } + } + } catch (Exception e) { + log.error("[processDataSafely][设备({}) 数据({}) 处理异常]", deviceId, identifier, e); + } + } + +} diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java index 4be6d3d..58e6122 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java @@ -4,9 +4,9 @@ import com.viewsh.module.iot.core.integration.constants.CleanOrderTopics; import com.viewsh.module.iot.core.integration.event.clean.CleanOrderArriveEvent; import com.viewsh.module.iot.core.integration.event.clean.CleanOrderAuditEvent; import com.viewsh.module.iot.dal.dataobject.integration.clean.BeaconPresenceConfig; +import com.viewsh.module.iot.dal.redis.clean.BadgeDeviceStatusRedisDAO; import com.viewsh.module.iot.dal.redis.clean.BeaconArrivedTimeRedisDAO; import com.viewsh.module.iot.dal.redis.clean.BeaconRssiWindowRedisDAO; -import com.viewsh.module.iot.dal.redis.clean.DeviceCurrentOrderRedisDAO; import com.viewsh.module.iot.dal.redis.clean.SignalLossRedisDAO; import com.viewsh.module.iot.service.integration.clean.CleanOrderIntegrationConfigService; import com.viewsh.module.iot.service.rule.clean.detector.RssiSlidingWindowDetector; @@ -42,7 +42,7 @@ public class BeaconDetectionRuleProcessor { private SignalLossRedisDAO signalLossRedisDAO; @Resource - private DeviceCurrentOrderRedisDAO deviceCurrentOrderRedisDAO; + private BadgeDeviceStatusRedisDAO badgeDeviceStatusRedisDAO; @Resource private CleanOrderIntegrationConfigService configService; @@ -110,8 +110,8 @@ public class BeaconDetectionRuleProcessor { List window = windowRedisDAO.getWindow(deviceId, areaId); // 6. 获取设备当前工单状态 - DeviceCurrentOrderRedisDAO.OrderCacheInfo currentOrder = - deviceCurrentOrderRedisDAO.getCurrentOrder(deviceId); + BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = + badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); // 7. 确定当前状态 RssiSlidingWindowDetector.AreaState currentState = determineState(currentOrder, areaId); @@ -284,7 +284,7 @@ public class BeaconDetectionRuleProcessor { * 确定当前状态 */ private RssiSlidingWindowDetector.AreaState determineState( - DeviceCurrentOrderRedisDAO.OrderCacheInfo currentOrder, Long areaId) { + BadgeDeviceStatusRedisDAO.OrderInfo currentOrder, Long areaId) { if (currentOrder == null) { return RssiSlidingWindowDetector.AreaState.OUT_AREA; @@ -308,8 +308,8 @@ public class BeaconDetectionRuleProcessor { * @return true-工单切换场景,false-正常离岗场景 */ private boolean isSwitchingOrder(Long deviceId, Long areaId) { - DeviceCurrentOrderRedisDAO.OrderCacheInfo currentOrder = - deviceCurrentOrderRedisDAO.getCurrentOrder(deviceId); + BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = + badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); return currentOrder != null && !currentOrder.getAreaId().equals(areaId); } } diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/ButtonEventRuleProcessor.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/ButtonEventRuleProcessor.java index 9d655b9..c8ef8e7 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/ButtonEventRuleProcessor.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/ButtonEventRuleProcessor.java @@ -2,7 +2,7 @@ package com.viewsh.module.iot.service.rule.clean.processor; import com.viewsh.module.iot.core.integration.constants.CleanOrderTopics; import com.viewsh.module.iot.dal.dataobject.integration.clean.ButtonEventConfig; -import com.viewsh.module.iot.dal.redis.clean.DeviceCurrentOrderRedisDAO; +import com.viewsh.module.iot.dal.redis.clean.BadgeDeviceStatusRedisDAO; import com.viewsh.module.iot.service.integration.clean.CleanOrderIntegrationConfigService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -34,7 +34,7 @@ public class ButtonEventRuleProcessor { private CleanOrderIntegrationConfigService configService; @Resource - private DeviceCurrentOrderRedisDAO deviceCurrentOrderRedisDAO; + private BadgeDeviceStatusRedisDAO badgeDeviceStatusRedisDAO; @Resource private RocketMQTemplate rocketMQTemplate; @@ -104,20 +104,15 @@ public class ButtonEventRuleProcessor { log.info("[ButtonEvent] 确认键按下:deviceId={}, buttonId={}", deviceId, buttonId); // 1. 查询设备当前工单 - DeviceCurrentOrderRedisDAO.OrderCacheInfo currentOrderJson = deviceCurrentOrderRedisDAO.getCurrentOrder(deviceId); - if (currentOrderJson == null) { + BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); + if (currentOrder == null) { log.warn("[ButtonEvent] 设备无当前工单,跳过确认:deviceId={}", deviceId); return; } - // 2. 解析工单ID(简单解析,生产环境可用 JSON 库) - Long orderId = extractOrderIdFromJson(currentOrderJson.getOrderId().toString()); - if (orderId == null) { - log.warn("[ButtonEvent] 工单ID解析失败:deviceId={}, json={}", deviceId, currentOrderJson); - return; - } + Long orderId = currentOrder.getOrderId(); - // 3. 防重复检查(短时间内同一工单的确认操作去重) + // 2. 防重复检查(短时间内同一工单的确认操作去重) String dedupKey = String.format("iot:clean:button:dedup:confirm:%s:%s", deviceId, orderId); Boolean firstTime = stringRedisTemplate.opsForValue() .setIfAbsent(dedupKey, "1", 10, java.util.concurrent.TimeUnit.SECONDS); @@ -127,7 +122,7 @@ public class ButtonEventRuleProcessor { return; } - // 4. 发布工单确认事件 + // 3. 发布工单确认事件 publishConfirmEvent(configWrapper, orderId, buttonId); log.info("[ButtonEvent] 发布工单确认事件:deviceId={}, orderId={}", deviceId, orderId); @@ -145,25 +140,18 @@ public class ButtonEventRuleProcessor { log.info("[ButtonEvent] 查询键按下:deviceId={}, buttonId={}", deviceId, buttonId); // 1. 查询设备当前工单 - DeviceCurrentOrderRedisDAO.OrderCacheInfo currentOrderJson = deviceCurrentOrderRedisDAO.getCurrentOrder(deviceId); - if (currentOrderJson == null) { + BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); + if (currentOrder == null) { log.info("[ButtonEvent] 设备无当前工单:deviceId={}", deviceId); // 发布查询结果事件(无工单) publishQueryEvent(configWrapper, null, buttonId, "当前无工单"); return; } - // 2. 解析工单信息 - Long orderId = extractOrderIdFromJson(currentOrderJson.getOrderId().toString()); - if (orderId == null) { - log.warn("[ButtonEvent] 工单ID解析失败:deviceId={}, json={}", deviceId, currentOrderJson); - return; - } + // 2. 发布查询事件 + publishQueryEvent(configWrapper, currentOrder.getOrderId(), buttonId, "查询当前工单"); - // 3. 发布查询事件 - publishQueryEvent(configWrapper, orderId, buttonId, "查询当前工单"); - - log.info("[ButtonEvent] 发布工单查询事件:deviceId={}, orderId={}", deviceId, orderId); + log.info("[ButtonEvent] 发布工单查询事件:deviceId={}, orderId={}", deviceId, currentOrder.getOrderId()); } /** @@ -225,41 +213,6 @@ public class ButtonEventRuleProcessor { } } - /** - * 从 JSON 字符串中提取工单ID - *

- * 简单实现,生产环境建议使用 Jackson 或 Gson - */ - private Long extractOrderIdFromJson(String json) { - try { - // 简单解析:查找 "orderId":12345 - int orderIdIndex = json.indexOf("\"orderId\""); - if (orderIdIndex == -1) { - return null; - } - - int colonIndex = json.indexOf(":", orderIdIndex); - if (colonIndex == -1) { - return null; - } - - int endIndex = json.indexOf(",", colonIndex); - if (endIndex == -1) { - endIndex = json.indexOf("}", colonIndex); - } - - if (endIndex == -1) { - return null; - } - - String idStr = json.substring(colonIndex + 1, endIndex).trim(); - return Long.parseLong(idStr); - } catch (Exception e) { - log.error("[ButtonEvent] 解析工单ID失败:json={}", json, e); - return null; - } - } - /** * 获取配置包装器 */ diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/SignalLossRuleProcessor.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/SignalLossRuleProcessor.java index c05327a..b7163ac 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/SignalLossRuleProcessor.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/SignalLossRuleProcessor.java @@ -4,9 +4,9 @@ import com.viewsh.module.iot.core.integration.constants.CleanOrderTopics; import com.viewsh.module.iot.core.integration.event.clean.CleanOrderAuditEvent; import com.viewsh.module.iot.core.integration.event.clean.CleanOrderCompleteEvent; import com.viewsh.module.iot.dal.dataobject.integration.clean.BeaconPresenceConfig; +import com.viewsh.module.iot.dal.redis.clean.BadgeDeviceStatusRedisDAO; import com.viewsh.module.iot.dal.redis.clean.BeaconArrivedTimeRedisDAO; import com.viewsh.module.iot.dal.redis.clean.BeaconRssiWindowRedisDAO; -import com.viewsh.module.iot.dal.redis.clean.DeviceCurrentOrderRedisDAO; import com.viewsh.module.iot.dal.redis.clean.SignalLossRedisDAO; import com.viewsh.module.iot.service.integration.clean.CleanOrderIntegrationConfigService; import com.xxl.job.core.handler.annotation.XxlJob; @@ -43,7 +43,7 @@ public class SignalLossRuleProcessor { private BeaconRssiWindowRedisDAO windowRedisDAO; @Resource - private DeviceCurrentOrderRedisDAO deviceCurrentOrderRedisDAO; + private BadgeDeviceStatusRedisDAO badgeDeviceStatusRedisDAO; @Resource private CleanOrderIntegrationConfigService configService; @@ -85,12 +85,12 @@ public class SignalLossRuleProcessor { // 解析 deviceId 和 areaId // Key 格式:iot:clean:signal:loss:{deviceId}:{areaId} String[] parts = key.split(":"); - if (parts.length < 6) { - continue; - } - - Long deviceId = Long.parseLong(parts[4]); - Long areaId = Long.parseLong(parts[5]); + if (parts.length < 6) { + continue; + } + + Long deviceId = Long.parseLong(parts[4]); + Long areaId = Long.parseLong(parts[5]); // 检查超时 checkTimeoutForDevice(deviceId, areaId); @@ -227,8 +227,8 @@ public class SignalLossRuleProcessor { deviceId, areaId, durationMs); // 1. 获取当前工单 - DeviceCurrentOrderRedisDAO.OrderCacheInfo currentOrder = - deviceCurrentOrderRedisDAO.getCurrentOrder(deviceId); + BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = + badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); if (currentOrder == null) { log.warn("[SignalLoss] 设备无当前工单:deviceId={}", deviceId); @@ -332,8 +332,8 @@ public class SignalLossRuleProcessor { * @return true-工单切换场景,false-正常离岗场景 */ private boolean isSwitchingOrder(Long deviceId, Long areaId) { - DeviceCurrentOrderRedisDAO.OrderCacheInfo currentOrder = - deviceCurrentOrderRedisDAO.getCurrentOrder(deviceId); + BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = + badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); return currentOrder != null && !currentOrder.getAreaId().equals(areaId); } } diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/TrafficThresholdRuleProcessor.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/TrafficThresholdRuleProcessor.java index f907f66..063343b 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/TrafficThresholdRuleProcessor.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/TrafficThresholdRuleProcessor.java @@ -1,16 +1,14 @@ package com.viewsh.module.iot.service.rule.clean.processor; -import com.viewsh.framework.common.util.json.JsonUtils; import com.viewsh.module.iot.core.integration.constants.CleanOrderTopics; import com.viewsh.module.iot.core.integration.event.clean.CleanOrderCreateEvent; -import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig; import com.viewsh.module.iot.dal.dataobject.integration.clean.TrafficThresholdConfig; -import com.viewsh.module.iot.dal.redis.clean.DeviceCurrentOrderRedisDAO; import com.viewsh.module.iot.dal.redis.clean.TrafficCounterBaseRedisDAO; import com.viewsh.module.iot.service.integration.clean.CleanOrderIntegrationConfigService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Component; @@ -36,10 +34,10 @@ public class TrafficThresholdRuleProcessor { private TrafficCounterBaseRedisDAO trafficBaseRedisDAO; @Resource - private DeviceCurrentOrderRedisDAO deviceCurrentOrderRedisDAO; + private RocketMQTemplate rocketMQTemplate; @Resource - private RocketMQTemplate rocketMQTemplate; + private StringRedisTemplate stringRedisTemplate; /** * 处理客流属性上报 @@ -182,7 +180,4 @@ public class TrafficThresholdRuleProcessor { return null; } - - @Resource - private org.springframework.data.redis.core.StringRedisTemplate stringRedisTemplate; } diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/test/java/com/viewsh/module/iot/service/rule/clean/processor/SignalLossRuleProcessorTest.java b/viewsh-module-iot/viewsh-module-iot-server/src/test/java/com/viewsh/module/iot/service/rule/clean/processor/SignalLossRuleProcessorTest.java index f681d35..64e2638 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/test/java/com/viewsh/module/iot/service/rule/clean/processor/SignalLossRuleProcessorTest.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/test/java/com/viewsh/module/iot/service/rule/clean/processor/SignalLossRuleProcessorTest.java @@ -3,9 +3,9 @@ package com.viewsh.module.iot.service.rule.clean.processor; import com.viewsh.module.iot.core.integration.constants.CleanOrderTopics; import com.viewsh.module.iot.dal.dataobject.integration.clean.BeaconPresenceConfig; import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig; +import com.viewsh.module.iot.dal.redis.clean.BadgeDeviceStatusRedisDAO; import com.viewsh.module.iot.dal.redis.clean.BeaconArrivedTimeRedisDAO; import com.viewsh.module.iot.dal.redis.clean.BeaconRssiWindowRedisDAO; -import com.viewsh.module.iot.dal.redis.clean.DeviceCurrentOrderRedisDAO; import com.viewsh.module.iot.dal.redis.clean.SignalLossRedisDAO; import com.viewsh.module.iot.service.integration.clean.CleanOrderIntegrationConfigService; import org.apache.rocketmq.spring.core.RocketMQTemplate; @@ -37,7 +37,7 @@ class SignalLossRuleProcessorTest { @Mock private BeaconRssiWindowRedisDAO windowRedisDAO; @Mock - private DeviceCurrentOrderRedisDAO deviceCurrentOrderRedisDAO; + private BadgeDeviceStatusRedisDAO badgeDeviceStatusRedisDAO; @Mock private CleanOrderIntegrationConfigService configService; @Mock @@ -71,10 +71,10 @@ class SignalLossRuleProcessorTest { when(arrivedTimeRedisDAO.getArrivedTime(DEVICE_ID, AREA_ID)).thenReturn(arrivedTime); // Mock Current Order - DeviceCurrentOrderRedisDAO.OrderCacheInfo orderInfo = new DeviceCurrentOrderRedisDAO.OrderCacheInfo(); + BadgeDeviceStatusRedisDAO.OrderInfo orderInfo = new BadgeDeviceStatusRedisDAO.OrderInfo(); orderInfo.setOrderId(500L); orderInfo.setAreaId(AREA_ID); // Same area, valid - when(deviceCurrentOrderRedisDAO.getCurrentOrder(DEVICE_ID)).thenReturn(orderInfo); + when(badgeDeviceStatusRedisDAO.getCurrentOrder(DEVICE_ID)).thenReturn(orderInfo); // Mock Config BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig(); diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/constants/CleanNotificationConstants.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/constants/CleanNotificationConstants.java index 03a025f..ce548d0 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/constants/CleanNotificationConstants.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/constants/CleanNotificationConstants.java @@ -1,10 +1,12 @@ package com.viewsh.module.ops.environment.constants; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; + /** * 保洁业务通知消息常量 *

* 统一管理所有通知相关的消息内容: - * - 语音播报内容 + * - 语音播报内容(带参数的模板) * - 站内信模板代码 * - 站内信模板参数 *

@@ -12,6 +14,7 @@ package com.viewsh.module.ops.environment.constants; * - 集中管理便于国际化 * - 避免硬编码散落在各处 * - 修改消息只需改此处 + * - 语音播报提供简短版和详细版,根据场景选择 * * @author lzh */ @@ -62,49 +65,129 @@ public class CleanNotificationConstants { } /** - * 语音播报内容 + * 语音播报模板 + *

+ * 所有模板支持参数化,使用 %s, %d 等占位符 + * 提供简短版(适合快速通知)和详细版(包含更多信息) */ - public static class VoiceMessage { + public static class VoiceTemplate { + + // ==================== 新工单播报 ==================== /** - * 新工单播报 + * 新工单播报(简短版) */ - public static final String NEW_ORDER = "您有1条新的待办工单"; + public static final String NEW_ORDER_SHORT = "您有新的待办工单"; /** - * 待办增加播报 - * 参数: {queueCount} - 总待办数 + * 新工单播报(详细版) + * 参数: {areaName} - 区域名称 */ - public static final String QUEUED_ORDER_FORMAT = "新增%d项待办,您共有%d个待办工单"; + public static final String NEW_ORDER_DETAIL = "新工单已派发,作业区域:%s"; /** - * 简化的待办播报(用于快速通知) + * 新工单播报(完整版) + * 参数: {areaName} - 区域名称, {orderTitle} - 工单标题(截断) */ - public static final String QUEUED_ORDER_SIMPLE = "您有新的待办工单"; + public static final String NEW_ORDER_FULL = "新工单:%s,作业区域:%s"; + + // ==================== 工单确认播报 ==================== + + /** + * 工单确认播报(简短版) + */ + public static final String ORDER_CONFIRMED_SHORT = "工单已确认"; + + /** + * 工单确认播报(标准版) + * 参数: {areaName} - 区域名称 + */ + public static final String ORDER_CONFIRMED = "工单已确认,请前往%s开始作业"; + + // ==================== 工单到岗播报 ==================== + + /** + * 工单到岗播报(简短版) + */ + public static final String ORDER_ARRIVED_SHORT = "已到达作业区域"; + + /** + * 工单到岗播报(标准版) + */ + public static final String ORDER_ARRIVED = "已到达作业区域,请开始清洁作业"; + + // ==================== 工单完成播报 ==================== + + /** + * 工单完成播报 + */ + public static final String ORDER_COMPLETED = "工单已完成,请拍照上传"; + + // ==================== 待办增加播报 ==================== + + /** + * 待办增加播报(简短版) + */ + public static final String QUEUED_ORDER_SHORT = "您有新的待办工单"; + + /** + * 待办增加播报(详细版) + * 参数: {totalCount} - 总待办数 + */ + public static final String QUEUED_ORDER_DETAIL = "您现在共有%d个待办工单"; + + // ==================== 下一任务播报 ==================== /** * 下一任务播报 - * 参数: {queueCount} - 总待办数, {orderTitle} - 第一个任务标题 + * 参数: {orderTitle} - 任务标题 */ - public static final String NEXT_TASK_FORMAT = "待办工单总数%d个,第一位待办工单%s"; + public static final String NEXT_TASK = "下一任务:%s"; + + // ==================== P0紧急任务播报 ==================== /** - * P0紧急任务播报 + * P0紧急任务播报(简短版) + */ + public static final String P0_SHORT = "紧急任务!请立即处理"; + + /** + * P0紧急任务播报(详细版) * 参数: {orderCode} - 工单编号 */ - public static final String PRIORITY_UPGRADE_FORMAT = "紧急任务!工单%s已升级为P0,请立即处理"; + public static final String P0_DETAIL = "紧急任务!工单%s请立即处理"; + + // ==================== 任务恢复播报 ==================== /** - * 任务恢复播报 + * 任务恢复播报(简短版) + */ + public static final String TASK_RESUMED_SHORT = "任务已恢复"; + + /** + * 任务恢复播报(详细版) * 参数: {areaName} - 区域名称 */ - public static final String TASK_RESUMED_FORMAT = "任务恢复,请继续完成%s的清洁"; + public static final String TASK_RESUMED = "任务已恢复,请继续完成%s的清洁"; + + // ==================== 按���查询播报 ==================== /** - * 工单完成播报(巡检员) - * 参数: {orderCode} - 工单编号 + * 按键查询播报(有工单时) + * 参数: {orderTitle} - 当前工单标题 */ - public static final String ORDER_COMPLETED_FORMAT = "保洁工单%s已完成,请前往验收"; + public static final String QUERY_HAS_ORDER = "当前工单:%s"; + + /** + * 按键查询播报(无工单时) + */ + public static final String QUERY_NO_ORDER = "暂无待办工单"; + + /** + * 按键查询播报(待办数量提示) + * 参数: {count} - 待办数量 + */ + public static final String QUEUE_COUNT = "您还有%d个待办工单"; } /** @@ -142,63 +225,157 @@ public class CleanNotificationConstants { } /** - * 语音播报辅助方法 + * 语音播报构建器 + *

+ * 根据工单信息智能生成语音播报内容 */ - public static class VoiceHelper { + public static class VoiceBuilder { /** - * 格式化待办增加播报 + * 默认区域名称 + */ + private static final String DEFAULT_AREA_NAME = "作业区域"; + + /** + * 标题最大长度(超长会截断) + */ + private static final int MAX_TITLE_LENGTH = 10; + + /** + * 构建新工单播报 * - * @param newCount 新增数量 - * @param totalCount 总数量 + * @param order 工单信息 + * @param useDetail 是否使用详细版 * @return 播报内容 */ - public static String formatQueuedOrder(int newCount, int totalCount) { - if (totalCount <= 1) { - return VoiceMessage.QUEUED_ORDER_SIMPLE; + public static String buildNewOrder(OpsOrderDO order, boolean useDetail) { + if (order == null) { + return VoiceTemplate.NEW_ORDER_SHORT; } - return String.format(VoiceMessage.QUEUED_ORDER_FORMAT, newCount, totalCount); + + if (!useDetail) { + return VoiceTemplate.NEW_ORDER_SHORT; + } + + String areaName = order.getLocation() != null ? order.getLocation() : DEFAULT_AREA_NAME; + String title = truncateTitle(order.getTitle()); + + return String.format(VoiceTemplate.NEW_ORDER_FULL, title, areaName); } /** - * 格式化下一任务播报 + * 构建工单确认播报 + * + * @param order 工单信息 + * @return 播报内容 + */ + public static String buildOrderConfirmed(OpsOrderDO order) { + if (order == null) { + return VoiceTemplate.ORDER_CONFIRMED_SHORT; + } + + String areaName = order.getLocation() != null ? order.getLocation() : DEFAULT_AREA_NAME; + return String.format(VoiceTemplate.ORDER_CONFIRMED, areaName); + } + + /** + * 构建待办增加播报 * * @param totalCount 总待办数 - * @param orderTitle 第一个任务标题 * @return 播报内容 */ - public static String formatNextTask(int totalCount, String orderTitle) { - return String.format(VoiceMessage.NEXT_TASK_FORMAT, totalCount, orderTitle); + public static String buildQueuedOrder(int totalCount) { + if (totalCount <= 1) { + return VoiceTemplate.QUEUED_ORDER_SHORT; + } + return String.format(VoiceTemplate.QUEUED_ORDER_DETAIL, totalCount); } /** - * 格式化P0紧急任务��报 + * 构建下一任务播报 + * + * @param orderTitle 任务标题 + * @return 播报内容 + */ + public static String buildNextTask(String orderTitle) { + if (orderTitle == null || orderTitle.isEmpty()) { + return "请继续处理下一任务"; + } + String title = truncateTitle(orderTitle); + return String.format(VoiceTemplate.NEXT_TASK, title); + } + + /** + * 构建P0紧急任务播报 * * @param orderCode 工单编号 * @return 播报内容 */ - public static String formatPriorityUpgrade(String orderCode) { - return String.format(VoiceMessage.PRIORITY_UPGRADE_FORMAT, orderCode); + public static String buildPriorityUpgrade(String orderCode) { + if (orderCode != null && !orderCode.isEmpty()) { + return String.format(VoiceTemplate.P0_DETAIL, orderCode); + } + return VoiceTemplate.P0_SHORT; } /** - * 格式化任务恢复播报 + * 构建任务恢复播报 * * @param areaName 区域名称 * @return 播报内容 */ - public static String formatTaskResumed(String areaName) { - return String.format(VoiceMessage.TASK_RESUMED_FORMAT, areaName); + public static String buildTaskResumed(String areaName) { + if (areaName == null || areaName.isEmpty()) { + return VoiceTemplate.TASK_RESUMED_SHORT; + } + return String.format(VoiceTemplate.TASK_RESUMED, areaName); } /** - * 格式化工单完成播报 + * 构建按键查询播报 * - * @param orderCode 工单编号 + * @param currentOrderTitle 当前工单标题 + * @param pendingCount 待办数量(不含当前工单) * @return 播报内容 */ - public static String formatOrderCompleted(String orderCode) { - return String.format(VoiceMessage.ORDER_COMPLETED_FORMAT, orderCode); + public static String buildQuery(String currentOrderTitle, int pendingCount) { + // 优先显示当前正在处理的工单 + if (currentOrderTitle != null && !currentOrderTitle.isEmpty()) { + String title = truncateTitle(currentOrderTitle); + return String.format(VoiceTemplate.QUERY_HAS_ORDER, title); + } + // 没有当前工单,显示待办数量 + if (pendingCount > 0) { + return String.format(VoiceTemplate.QUEUE_COUNT, pendingCount); + } + // 完全没有工单 + return VoiceTemplate.QUERY_NO_ORDER; + } + + /** + * 截断标题到指定长度 + * + * @param title 原标题 + * @return 截断后的标题 + */ + private static String truncateTitle(String title) { + if (title == null || title.isEmpty()) { + return "保洁工单"; + } + if (title.length() <= MAX_TITLE_LENGTH) { + return title; + } + return title.substring(0, MAX_TITLE_LENGTH); + } + + /** + * 获取默认区域名称 + * + * @param location 原位置信息 + * @return 区域名称 + */ + public static String getAreaName(String location) { + return (location != null && !location.isEmpty()) ? location : DEFAULT_AREA_NAME; } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/dataobject/CleanOrderAutoCreateReqDTO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/dataobject/CleanOrderAutoCreateReqDTO.java index 49788da..9c1a7a2 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/dataobject/CleanOrderAutoCreateReqDTO.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/dataobject/CleanOrderAutoCreateReqDTO.java @@ -8,6 +8,8 @@ import lombok.EqualsAndHashCode; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; +import java.util.Map; + /** * 保洁自动工单创建请求 DTO * @@ -44,4 +46,7 @@ public class CleanOrderAutoCreateReqDTO extends OpsOrderCreateReqDTO { @Schema(description = "触发设备Key(冗余,便于查询)") private String triggerDeviceKey; + @Schema(description = "触发数据(如客流值、阈值等,用于后续处理)") + private Map triggerData; + } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/dataobject/workorder/OpsOrderCleanExtDO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/dataobject/workorder/OpsOrderCleanExtDO.java index c68f12f..7599c05 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/dataobject/workorder/OpsOrderCleanExtDO.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/dataobject/workorder/OpsOrderCleanExtDO.java @@ -42,6 +42,14 @@ public class OpsOrderCleanExtDO extends BaseDO { * 预计作业时长(分钟) */ private Integer expectedDuration; + /** + * 实际下发时间 + */ + private LocalDateTime dispatchedTime; + /** + * 首次下发时间(用于计算响应时长,不受暂停影响) + */ + private LocalDateTime firstDispatchedTime; /** * 实际到岗时间 */ diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/mysql/workorder/OpsOrderCleanExtMapper.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/mysql/workorder/OpsOrderCleanExtMapper.java index da47f82..f4848f5 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/mysql/workorder/OpsOrderCleanExtMapper.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/mysql/workorder/OpsOrderCleanExtMapper.java @@ -38,4 +38,26 @@ public interface OpsOrderCleanExtMapper extends BaseMapperX } } + /** + * 插入或选择性更新扩展信息 + *

+ * 只更新非空字段,且保留 firstDispatchedTime(已存在时不覆盖) + * + * @param entity 扩展信息实体 + * @return 影响行数 + */ + default int insertOrUpdateSelective(OpsOrderCleanExtDO entity) { + OpsOrderCleanExtDO existing = selectByOpsOrderId(entity.getOpsOrderId()); + if (existing == null) { + return insert(entity); + } else { + entity.setId(existing.getId()); + // 保留首次下发时间 + if (existing.getFirstDispatchedTime() != null && entity.getFirstDispatchedTime() != null) { + entity.setFirstDispatchedTime(existing.getFirstDispatchedTime()); + } + return updateById(entity); + } + } + } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderArriveEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderArriveEventHandler.java index 068da7b..1c649fd 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderArriveEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderArriveEventHandler.java @@ -8,9 +8,6 @@ import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.enums.OperatorTypeEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.environment.integration.dto.CleanOrderArriveEventDTO; -import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; -import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord; -import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; @@ -28,9 +25,30 @@ import java.util.concurrent.TimeUnit; *

* 订阅 IoT 模块发布的保洁工单到岗事件 *

- * RocketMQ 配置: - * - Topic: ops-order-arrive - * - ConsumerGroup: ops-clean-order-arrive-group + * 调用链路: + *

+ * IoT 发布 RocketMQ 消息 (ops-order-arrive)
+ *     ↓
+ * CleanOrderArriveEventHandler.onMessage()
+ *     - 幂等性检查
+ *     - 状态检查
+ *     ↓
+ * orderLifecycleManager.transition(ARRIVED) [@Transactional]
+ *     - 更新工单状态
+ *     - 发布 OrderStateChangedEvent
+ *     ↓
+ * 事务提交
+ *     ↓
+ * CleanOrderEventListener.handleArrived() [@Async, AFTER_COMMIT]
+ *     - 记录到岗时间到扩展表
+ *     - 更新设备工单关联信息(areaId, beaconMac)
+ *     - 记录业务日志
+ * 
+ *

+ * 设计说明: + * - Handler 只负责消息接收和状态转换 + * - 业务日志、设备缓存更新由 CleanOrderEventListener 在事务提交后处理 + * - BadgeDeviceStatusServiceImpl 在 BEFORE_COMMIT 阶段跳过 ARRIVED(由 Listener 处理完整信息) * * @author AI */ @@ -54,16 +72,6 @@ public class CleanOrderArriveEventHandler implements RocketMQListener { */ private static final int DEDUP_TTL_SECONDS = 300; - /** - * 设备当前工单缓存 Key 模式 - */ - private static final String ORDER_CACHE_KEY_PATTERN = "ops:clean:device:order:%s"; - - /** - * 工单缓存 TTL(秒) - */ - private static final int ORDER_CACHE_TTL_SECONDS = 3600; - @Resource private ObjectMapper objectMapper; @@ -76,9 +84,6 @@ public class CleanOrderArriveEventHandler implements RocketMQListener { @Resource private OrderLifecycleManager orderLifecycleManager; - @Resource - private EventLogRecorder eventLogRecorder; - @Override public void onMessage(String message) { try { @@ -126,12 +131,7 @@ public class CleanOrderArriveEventHandler implements RocketMQListener { return; } - // 3. 更新工单的设备信息(扩展字段) - order.setAssigneeDeviceId(event.getDeviceId()); - order.setAssigneeDeviceKey(event.getDeviceKey()); - opsOrderMapper.updateById(order); - - // 4. 构建状态转换请求 + // 3. 构建状态转换请求(包含完整信息,供 Listener 使用) Map payload = new HashMap<>(); payload.put("deviceId", event.getDeviceId()); payload.put("deviceKey", event.getDeviceKey()); @@ -148,87 +148,11 @@ public class CleanOrderArriveEventHandler implements RocketMQListener { .reason("蓝牙信标自动到岗确认") .payload(payload) .build(); - - // 5. 通过生命周期管理器执行状态转换(DISPATCHED/CONFIRMED -> ARRIVED) + // 4. 执行状态转换 + // 注意:业务日志和设备缓存更新由 CleanOrderEventListener 在 AFTER_COMMIT 阶段处理 orderLifecycleManager.transition(request); - // 6. 记录业务日志 - recordOrderArrivedLog(event, request); - - // 7. 更新 Redis 缓存(设备当前工单) - cacheDeviceCurrentOrder(event); - log.info("[CleanOrderArriveEventHandler] 工单到岗成功: eventId={}, orderId={}, deviceId={}", event.getEventId(), event.getOrderId(), event.getDeviceId()); } - - /** - * 缓存设备当前工单 - *

- * 供 IoT 模块的规则处理器查询 - */ - private void cacheDeviceCurrentOrder(CleanOrderArriveEventDTO event) { - try { - String cacheKey = String.format(ORDER_CACHE_KEY_PATTERN, event.getDeviceId()); - - // 构建缓存数据 - StringBuilder cacheData = new StringBuilder(); - cacheData.append("{"); - cacheData.append("\"orderId\":").append(event.getOrderId()).append(","); - cacheData.append("\"status\":\"").append(WorkOrderStatusEnum.ARRIVED.getStatus()).append("\","); - cacheData.append("\"areaId\":").append(event.getAreaId()); - - // 如果有信标 MAC,也缓存 - if (event.getTriggerData() != null && event.getTriggerData().containsKey("beaconMac")) { - cacheData.append(",\"beaconMac\":\"").append(event.getTriggerData().get("beaconMac")).append("\""); - } - - cacheData.append("}"); - - // 写入 Redis - stringRedisTemplate.opsForValue().set( - cacheKey, - cacheData.toString(), - ORDER_CACHE_TTL_SECONDS, - TimeUnit.SECONDS - ); - - log.debug("[CleanOrderArriveEventHandler] 设备工单缓存已更新: deviceId={}, orderId={}", - event.getDeviceId(), event.getOrderId()); - } catch (Exception e) { - log.error("[CleanOrderArriveEventHandler] 设备工单缓存更新失败: deviceId={}", event.getDeviceId(), e); - } - } - - /** - * 记录工单到岗业务日志 - */ - private void recordOrderArrivedLog(CleanOrderArriveEventDTO event, OrderTransitionRequest request) { - try { - // 构建扩展信息 - Map extra = new HashMap<>(); - extra.put("eventId", event.getEventId()); - extra.put("triggerSource", event.getTriggerSource()); - extra.put("areaId", event.getAreaId()); - if (event.getTriggerData() != null) { - extra.putAll(event.getTriggerData()); - } - - // 记录日志 - eventLogRecorder.record(EventLogRecord.builder() - .module("clean") - .domain(EventDomain.BEACON) - .eventType("ORDER_ARRIVED") - .message(String.format("蓝牙信标自动到岗确认 [设备:%s, 区域:%d]", - event.getDeviceKey(), event.getAreaId())) - .targetId(event.getOrderId()) - .targetType("order") - .deviceId(event.getDeviceId()) - .payload(extra) - .build()); - - } catch (Exception e) { - log.warn("[CleanOrderArriveEventHandler] 记录业务日志失败: orderId={}", event.getOrderId(), e); - } - } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderAuditEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderAuditEventHandler.java index 5010e32..664afea 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderAuditEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderAuditEventHandler.java @@ -1,6 +1,7 @@ package com.viewsh.module.ops.environment.integration.consumer; import com.fasterxml.jackson.databind.ObjectMapper; +import com.viewsh.module.ops.environment.constants.CleanNotificationConstants; import com.viewsh.module.ops.environment.integration.dto.CleanOrderAuditEventDTO; import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService; import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; @@ -19,7 +20,6 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.Arrays; -import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -55,16 +55,6 @@ public class CleanOrderAuditEventHandler implements RocketMQListener { private static final int DEDUP_TTL_SECONDS = 300; private static final String TRIGGER_SOURCE_QUERY = "IOT_BUTTON_QUERY"; - private static final String DEFAULT_AREA_NAME = "当前区域"; - private static final String TTS_TEMPLATE_QUERY = "当前位置:%s。待办工单:%d个"; - - private static final List ACTIVE_STATUS_LIST = Arrays.asList( - WorkOrderStatusEnum.QUEUED.getStatus(), - WorkOrderStatusEnum.DISPATCHED.getStatus(), - WorkOrderStatusEnum.CONFIRMED.getStatus(), - WorkOrderStatusEnum.ARRIVED.getStatus(), - WorkOrderStatusEnum.PAUSED.getStatus() - ); @Resource private ObjectMapper objectMapper; @@ -173,23 +163,28 @@ public class CleanOrderAuditEventHandler implements RocketMQListener { return; } - // 1. 获取当前区域名称 - String areaName = DEFAULT_AREA_NAME; - if (event.getOrderId() != null) { - OpsOrderDO order = opsOrderMapper.selectById(event.getOrderId()); - if (order != null && order.getLocation() != null) { - areaName = order.getLocation(); - } + // 1. 获取当前正在处理的工单(DISPATCHED, CONFIRMED, ARRIVED 状态) + String currentOrderTitle = null; + OpsOrderDO currentOrder = opsOrderMapper.selectOne(new LambdaQueryWrapperX() + .eq(OpsOrderDO::getAssigneeDeviceId, deviceId) + .in(OpsOrderDO::getStatus, Arrays.asList( + WorkOrderStatusEnum.DISPATCHED.getStatus(), + WorkOrderStatusEnum.CONFIRMED.getStatus(), + WorkOrderStatusEnum.ARRIVED.getStatus())) + .orderByAsc(OpsOrderDO::getId) + .last("LIMIT 1")); + + if (currentOrder != null && currentOrder.getTitle() != null) { + currentOrderTitle = currentOrder.getTitle(); } - // 2. 查询待办工单数量 - // status IN (QUEUED, DISPATCHED, CONFIRMED, ARRIVED, PAUSED) - Long count = opsOrderMapper.selectCount(new LambdaQueryWrapperX() + // 2. 查询待办工单数量(QUEUED 状态,不含当前处理中工单) + Long pendingCount = opsOrderMapper.selectCount(new LambdaQueryWrapperX() .eq(OpsOrderDO::getAssigneeDeviceId, deviceId) - .in(OpsOrderDO::getStatus, ACTIVE_STATUS_LIST)); + .eq(OpsOrderDO::getStatus, WorkOrderStatusEnum.QUEUED.getStatus())); - // 3. 构建 TTS 文本 - String ttsText = String.format(TTS_TEMPLATE_QUERY, areaName, count); + // 3. 构建 TTS 文本(使用统一模板构建器) + String ttsText = CleanNotificationConstants.VoiceBuilder.buildQuery(currentOrderTitle, pendingCount.intValue()); // 4. 下发 TTS sendTts(deviceId, ttsText); diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCompleteEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCompleteEventHandler.java index 904a0c3..5674d62 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCompleteEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCompleteEventHandler.java @@ -8,10 +8,6 @@ import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.enums.OperatorTypeEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.environment.integration.dto.CleanOrderCompleteEventDTO; -import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService; -import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; -import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord; -import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; @@ -29,9 +25,36 @@ import java.util.concurrent.TimeUnit; *

* 订阅 IoT 模块发布的保洁工单完成事件 *

- * RocketMQ 配置: - * - Topic: ops-order-complete - * - ConsumerGroup: ops-clean-order-complete-group + * 调用链路: + *

+ * IoT 发布 RocketMQ 消息 (ops-order-complete)
+ *     ↓
+ * CleanOrderCompleteEventHandler.onMessage()
+ *     - 幂等性检查
+ *     - 状态检查
+ *     ↓
+ * orderLifecycleManager.transition(COMPLETED) [@Transactional]
+ *     - 更新工单状态
+ *     - 发布 OrderStateChangedEvent
+ *     ↓
+ * 事务提交
+ *     ↓
+ * BadgeDeviceStatusEventListener.onOrderStateChanged() [BEFORE_COMMIT]
+ *     - 清除设备工单关联
+ *     - 检查等待任务,决定设备状态 (BUSY/IDLE)
+ *     ↓
+ * CleanOrderEventListener.handleCompleted() [AFTER_COMMIT]
+ *     - 记录完成时间到扩展表
+ *     - 记录业务日志
+ * CleanOrderEventListener.onOrderCompleted() [AFTER_COMMIT]
+ *     - 自动调度下一个任务
+ *     - 发送完成通知
+ * 
+ *

+ * 设计说明: + * - Handler 只负责消息接收和状态转换 + * - 设备状态由 BadgeDeviceStatusEventListener 在 BEFORE_COMMIT 阶段处理 + * - 业务日志、自动调度由 CleanOrderEventListener 在 AFTER_COMMIT 阶段处理 * * @author AI */ @@ -55,11 +78,6 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener */ private static final int DEDUP_TTL_SECONDS = 300; - /** - * 设备当前工单缓存 Key 模式 - */ - private static final String ORDER_CACHE_KEY_PATTERN = "ops:clean:device:order:%s"; - @Resource private ObjectMapper objectMapper; @@ -72,11 +90,6 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener @Resource private OrderLifecycleManager orderLifecycleManager; - @Resource - private CleanOrderService cleanOrderService; - - @Resource - private EventLogRecorder eventLogRecorder; @Override public void onMessage(String message) { @@ -146,23 +159,12 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener .payload(payload) .build(); - // 5. 通过生命周期管理器执行状态转换(ARRIVED -> COMPLETED) - orderLifecycleManager.completeOrder(event.getOrderId(), null, remark); + // 5. 执行状态转换 + // 注意:设备状态、业务日志、自动调度由事件监听器处理 + orderLifecycleManager.transition(request); - // 6. 记录业务日志 - recordOrderCompletedLog(event, order, remark); - - // 7. 清除 Redis 缓存(设备当前工单) - clearDeviceCurrentOrder(event.getDeviceId()); - - // 8. 自动调度下一个任务(优先恢复被中断的任务) - if (order.getAssigneeId() != null) { - cleanOrderService.autoDispatchNextOrder(event.getOrderId(), order.getAssigneeId()); - } - - log.info("[CleanOrderCompleteEventHandler] 工单完成成功: eventId={}, orderId={}, assigneeId={}, duration={}ms", - event.getEventId(), event.getOrderId(), order.getAssigneeId(), - event.getTriggerData() != null ? event.getTriggerData().get("durationMs") : "N/A"); + log.info("[CleanOrderCompleteEventHandler] 工单完成成功: eventId={}, orderId={}", + event.getEventId(), event.getOrderId()); } /** @@ -183,62 +185,4 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener return remark.toString(); } - /** - * 清除设备当前工单缓存 - */ - private void clearDeviceCurrentOrder(Long deviceId) { - try { - String cacheKey = String.format(ORDER_CACHE_KEY_PATTERN, deviceId); - stringRedisTemplate.delete(cacheKey); - - log.debug("[CleanOrderCompleteEventHandler] 设备工单缓存已清除: deviceId={}", deviceId); - } catch (Exception e) { - log.error("[CleanOrderCompleteEventHandler] 设备工单缓存清除失败: deviceId={}", deviceId, e); - } - } - - /** - * 记录工单完成业务日志 - */ - private void recordOrderCompletedLog(CleanOrderCompleteEventDTO event, OpsOrderDO order, String remark) { - try { - // 构建扩展信息 - Map extra = new HashMap<>(); - extra.put("eventId", event.getEventId()); - extra.put("triggerSource", event.getTriggerSource()); - extra.put("areaId", event.getAreaId()); - extra.put("completionReason", event.getTriggerData() != null ? - event.getTriggerData().get("completionReason") : "SIGNAL_LOSS_TIMEOUT"); - if (event.getTriggerData() != null) { - extra.putAll(event.getTriggerData()); - } - - // 计算作业时长(分钟) - String durationInfo = ""; - if (event.getTriggerData() != null && event.getTriggerData().containsKey("durationMs")) { - Object durationMs = event.getTriggerData().get("durationMs"); - if (durationMs != null) { - long durationMinutes = ((Number) durationMs).longValue() / 60000; - durationInfo = String.format(",作业时长: %d分钟", durationMinutes); - extra.put("durationMinutes", durationMinutes); - } - } - - // 记录日志 - eventLogRecorder.record(EventLogRecord.builder() - .module("clean") - .domain(EventDomain.BEACON) - .eventType("ORDER_COMPLETED") - .message("信号丢失超时自动完成 [设备:" + event.getDeviceKey() + durationInfo + "]") - .targetId(event.getOrderId()) - .targetType("order") - .deviceId(event.getDeviceId()) - .personId(order.getAssigneeId()) - .payload(extra) - .build()); - - } catch (Exception e) { - log.warn("[CleanOrderCompleteEventHandler] 记录业务日志失败: orderId={}", event.getOrderId(), e); - } - } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderConfirmEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderConfirmEventHandler.java index c064e8d..c2778b7 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderConfirmEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderConfirmEventHandler.java @@ -2,15 +2,12 @@ package com.viewsh.module.ops.environment.integration.consumer; import com.fasterxml.jackson.databind.ObjectMapper; import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager; -import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService; import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest; import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.enums.OperatorTypeEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.environment.integration.dto.CleanOrderConfirmEventDTO; -import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; -import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; @@ -25,6 +22,30 @@ import java.util.concurrent.TimeUnit; * 保洁工单确认事件消费者 *

* 订阅 IoT 模块发布的工单确认事件(如:工牌按键确认) + *

+ * 调用链路: + *

+ * IoT 发布 RocketMQ 消息 (ops-order-confirm)
+ *     ↓
+ * CleanOrderConfirmEventHandler.onMessage()
+ *     - 幂等性检查
+ *     - 状态检查
+ *     ↓
+ * orderLifecycleManager.transition(CONFIRMED) [@Transactional]
+ *     - 更新工单状态
+ *     - 发布 OrderStateChangedEvent
+ *     ↓
+ * 事务提交
+ *     ↓
+ * CleanOrderEventListener.onOrderStateChanged() [@TransactionalEventListener(AFTER_COMMIT)]
+ *     - 记录业务日志
+ *     - 发送 TTS "工单已确认,请前往作业区域开始作业"
+ * 
+ *

+ * 设计说明: + * - Handler 只负责消息接收和状态转换 + * - 日志记录和通知发送由 CleanOrderEventListener 在事务提交后处理 + * - 设备状态由 BadgeDeviceStatusServiceImpl 在 BEFORE_COMMIT 阶段同步 * * @author AI */ @@ -40,10 +61,6 @@ public class CleanOrderConfirmEventHandler implements RocketMQListener { private static final String DEDUP_KEY_PATTERN = "ops:clean:dedup:confirm:%s"; private static final int DEDUP_TTL_SECONDS = 300; - private static final String TTS_ORDER_IN_PROGRESS = "工单已在进行中"; - private static final String TTS_CANNOT_CONFIRM = "当前状态无法确认工单"; - private static final String TTS_CONFIRM_SUCCESS = "工单已确认,请前往作业区域开始作业"; - @Resource private ObjectMapper objectMapper; @@ -56,12 +73,6 @@ public class CleanOrderConfirmEventHandler implements RocketMQListener { @Resource private OrderLifecycleManager orderLifecycleManager; - @Resource - private EventLogRecorder eventLogRecorder; - - @Resource - private VoiceBroadcastService voiceBroadcastService; - @Override public void onMessage(String message) { try { @@ -88,60 +99,43 @@ public class CleanOrderConfirmEventHandler implements RocketMQListener { } WorkOrderStatusEnum currentStatus = WorkOrderStatusEnum.fromStatus(order.getStatus()); - log.info("[CleanOrderConfirmEventHandler] 收到确认事件: eventId={}, orderId={}, currentStatus={}", - eventId, orderId, currentStatus); + log.info("[CleanOrderConfirmEventHandler] 收到确认事件: eventId={}, orderId={}, currentStatus={}, deviceId={}", + eventId, orderId, currentStatus, event.getDeviceId()); // 4. 状态检查 - // 如果已在进行中 (CONFIRMED or ARRIVED),提示"工单已在进行中" + // 如果已在进行中 (CONFIRMED or ARRIVED),直接返回(TTS 由 Listener 处理) if (currentStatus == WorkOrderStatusEnum.CONFIRMED || currentStatus == WorkOrderStatusEnum.ARRIVED) { - sendTts(event.getDeviceId(), TTS_ORDER_IN_PROGRESS); + log.debug("[CleanOrderConfirmEventHandler] 工单已在进行中: orderId={}, status={}", orderId, currentStatus); return; } // 检查是否可以确认 if (!currentStatus.canConfirm()) { log.warn("[CleanOrderConfirmEventHandler] 当前状态无法确认工单: orderId={}, status={}", orderId, currentStatus); - sendTts(event.getDeviceId(), TTS_CANNOT_CONFIRM); return; } - // 5. 状态流转 -> CONFIRMED + // 5. 构建状态转换请求(包含 deviceId,供 Listener 使用) OrderTransitionRequest request = OrderTransitionRequest.builder() .orderId(orderId) .targetStatus(WorkOrderStatusEnum.CONFIRMED) .reason("工牌按键确认") .operatorType(OperatorTypeEnum.CLEANER) - .operatorId(order.getAssigneeId() != null ? order.getAssigneeId() : 0L) + .operatorId(event.getDeviceId() != null ? event.getDeviceId() : order.getAssigneeId()) .build(); + // 将 deviceId 放入 payload,供 Listener 使用 + request.putPayload("deviceId", event.getDeviceId()); + request.putPayload("triggerSource", "BADGE_BUTTON"); + // 6. 执行状态转换 + // 注意:日志记录和 TTS 由 CleanOrderEventListener 在 AFTER_COMMIT 阶段处理 orderLifecycleManager.transition(request); - // 6. 记录业务日志 - eventLogRecorder.info("clean", EventDomain.AUDIT, "ORDER_CONFIRM", - "工单已确认 (工牌按键)", orderId, event.getDeviceId(), order.getAssigneeId()); - - // 7. 发送 TTS 通知 - // "工单已确认,请前往{AreaName}开始作业" - sendTts(event.getDeviceId(), TTS_CONFIRM_SUCCESS); + log.info("[CleanOrderConfirmEventHandler] 工单确认成功: orderId={}, deviceId={}", orderId, event.getDeviceId()); } catch (Exception e) { log.error("[CleanOrderConfirmEventHandler] 消息处理失败: message={}", message, e); throw new RuntimeException("保洁工单确认事件处理失败", e); } } - - /** - * 发送 TTS 语音播报 - */ - private void sendTts(Long deviceId, String text) { - if (deviceId == null) { - return; - } - try { - voiceBroadcastService.broadcast(deviceId, text); - log.info("[CleanOrderConfirmEventHandler] TTS 下发成功: deviceId={}, text={}", deviceId, text); - } catch (Exception e) { - log.error("[CleanOrderConfirmEventHandler] TTS 发送失败: deviceId={}", deviceId, e); - } - } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java index c09a6b2..4148676 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java @@ -1,8 +1,6 @@ package com.viewsh.module.ops.environment.integration.consumer; import com.fasterxml.jackson.databind.ObjectMapper; -import com.viewsh.module.iot.api.device.IotDeviceControlApi; -import com.viewsh.module.iot.api.device.dto.ResetTrafficCounterReqDTO; import com.viewsh.module.ops.enums.PriorityEnum; import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO; import com.viewsh.module.ops.environment.integration.dto.CleanOrderCreateEventDTO; @@ -63,9 +61,6 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { @Resource private CleanOrderService cleanOrderService; - @Resource - private IotDeviceControlApi iotDeviceControlApi; - @Resource private EventLogRecorder eventLogRecorder; @@ -110,11 +105,10 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { createReq.setPriority(PriorityEnum.fromPriority(event.getPriority() != null ? Integer.parseInt(event.getPriority()) : 2).getPriority()); createReq.setAreaId(event.getAreaId()); - // location 字段由 areaId 自动关联,不需要在事件中传递 // 扩展字段 createReq.setExpectedDuration(calculateExpectedDuration(event)); - createReq.setCleaningType("ROUTINE"); // 可根据triggerSource动态设置 + createReq.setCleaningType("ROUTINE"); createReq.setDifficultyLevel(3); // IoT集成字段 @@ -122,6 +116,7 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { createReq.setTriggerRuleId(extractRuleId(event)); createReq.setTriggerDeviceId(event.getTriggerDeviceId()); createReq.setTriggerDeviceKey(event.getTriggerDeviceKey()); + createReq.setTriggerData(event.getTriggerData()); // 2. 创建工单(同时创建主表+扩展表) Long orderId = cleanOrderService.createAutoCleanOrder(createReq); @@ -129,11 +124,7 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { // 3. 记录业务日志 recordOrderCreatedLog(event, orderId, createReq); - // 4. 如果是客流触发的工单,重置客流计数器基准值 - // TODO: 需要优化这个工单是否创建成功,才重置 - if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) { - resetTrafficCounter(event, orderId); - } + // 注意:客流计数器重置已移至 CleanOrderEventListener,在事务提交后执行 log.info("[CleanOrderCreateEventHandler] 工单创建成功: eventId={}, orderId={}, areaId={}", event.getEventId(), orderId, event.getAreaId()); @@ -144,9 +135,8 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { */ private void recordOrderCreatedLog(CleanOrderCreateEventDTO event, Long orderId, CleanOrderAutoCreateReqDTO createReq) { try { - // 确定事件域和类型 + // 确定事件域 EventDomain domain = determineDomain(event.getTriggerSource()); - String eventType = "ORDER_CREATED"; // 构建扩展信息 Map extra = new HashMap<>(); @@ -159,19 +149,12 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { extra.putAll(event.getTriggerData()); } - // 记录日志 - eventLogRecorder.info("clean", domain, eventType, - buildLogMessage(event, createReq), - orderId, - event.getTriggerDeviceId(), - null); - - // 添加扩展信息 + // 记录日志(合并为一次调用) eventLogRecorder.record(EventLogRecord.builder() .module("clean") .domain(domain) - .eventType(eventType + "_DETAIL") - .message("工单创建详细数据") + .eventType("ORDER_CREATED") + .message(buildLogMessage(event, createReq)) .targetId(orderId) .targetType("order") .deviceId(event.getTriggerDeviceId()) @@ -213,56 +196,6 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { event.getTriggerDeviceKey(), event.getTriggerSource()); } - /** - * 重置客流计数器基准值 - *

- * 工单创建成功后,通知 IoT 模块重置客流计数器,以便统计下一波客流 - * - * @param event 工单创建事件 - * @param orderId 工单ID - */ - private void resetTrafficCounter(CleanOrderCreateEventDTO event, Long orderId) { - try { - // 获取当前客流值作为新的基准值 - Object currentCountObj = event.getTriggerData().get("actualCount"); - Object baseValueObj = event.getTriggerData().get("baseValue"); - - if (currentCountObj == null || baseValueObj == null) { - log.warn("[CleanOrderCreateEventHandler] 缺少客流数据,跳过重置: eventId={}, actualCount={}, baseValue={}", - event.getEventId(), currentCountObj, baseValueObj); - return; - } - - // 计算新的基准值 = 当前值 - Long currentCount = ((Number) currentCountObj).longValue(); - Long newBaseValue = currentCount; // 将当前客流设为新的基准值 - - // 构建重置请求 - ResetTrafficCounterReqDTO reqDTO = ResetTrafficCounterReqDTO.builder() - .deviceId(event.getTriggerDeviceId()) - .newBaseValue(newBaseValue) - .orderId(orderId) - .remark("工单创建后重置计数器") - .build(); - - // 调用 IoT 模块 RPC 接口 - var result = iotDeviceControlApi.resetTrafficCounter(reqDTO); - - if (result.getData() != null && result.getData()) { - log.info("[CleanOrderCreateEventHandler] 客流计数器重置成功: eventId={}, deviceId={}, newBaseValue={}", - event.getEventId(), event.getTriggerDeviceId(), newBaseValue); - } else { - log.warn("[CleanOrderCreateEventHandler] 客流计数器重置失败: eventId={}, deviceId={}", - event.getEventId(), event.getTriggerDeviceId()); - } - - } catch (Exception e) { - // 重置失败不应影响主流程 - log.error("[CleanOrderCreateEventHandler] 客流计数器重置异常: eventId={}, deviceId={}", - event.getEventId(), event.getTriggerDeviceId(), e); - } - } - /** * 生成工单标题 */ diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventHandler.java deleted file mode 100644 index 3de1748..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventHandler.java +++ /dev/null @@ -1,206 +0,0 @@ -package com.viewsh.module.ops.environment.integration.listener; - -import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService; -import com.viewsh.module.ops.core.event.OrderStateChangedEvent; -import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum; -import com.viewsh.module.ops.enums.WorkOrderStatusEnum; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -/** - * 工牌设备状态事件处理器 - *

- * 监听工单状态变更事件,同步更新工牌设备状态 - *

- * 设计说明: - * - 使用 Spring EventListener 监听工单状态变更事件 - * - 根据工单状态转换规则更新工牌设备状态 - * - 支持工单分配、开始、暂停、完成等场景 - * - * @author lzh - */ -@Slf4j -@Component -public class BadgeDeviceStatusEventHandler { - - @Resource - private BadgeDeviceStatusService badgeDeviceStatusService; - - /** - * 处理工单状态变更事件 - *

- * 状态映射规则: - * - PENDING → ASSIGNED: 不影响设备状态(设备可能还在处理其他任务) - * - ASSIGNED → ARRIVED: 设备状态 → BUSY(开始执行任务) - * - ARRIVED → PAUSED: 设备状态 → PAUSED(任务暂停) - * - PAUSED → ARRIVED/BUSY: 设备状态 → BUSY(任务恢复) - * - ARRIVED → COMPLETED: 设备状态 → IDLE(任务完成,检查是否还有等待任务) - * - 任意状态 → CANCELLED: 清除设备当前工单,检查是否还有等待任务 - * - * @param event 工单状态变更事件 - */ - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - @Async("ops-task-executor") - public void onOrderStateChanged(OrderStateChangedEvent event) { - try { - // 只处理保洁类型的工单 - if (!"CLEAN".equals(event.getOrderType())) { - return; - } - - log.debug("[BadgeDeviceStatusEventHandler] 收到工单状态变更事件: orderId={}, {} → {}, operatorId={}", - event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId()); - - // 获取设备ID(从 payload 或 operatorId 获取) - Long deviceId = getDeviceIdFromEvent(event); - if (deviceId == null) { - log.debug("[BadgeDeviceStatusEventHandler] 无法获取设备ID,跳过处理: orderId={}", event.getOrderId()); - return; - } - - // 根据状态转换更新设备状态 - handleStatusTransition(deviceId, event); - - } catch (Exception e) { - log.error("[BadgeDeviceStatusEventHandler] 处理工单状态变更事件失败: orderId={}", event.getOrderId(), e); - } - } - - /** - * 根据状态转换更新设备状态 - */ - private void handleStatusTransition(Long deviceId, OrderStateChangedEvent event) { - WorkOrderStatusEnum newStatus = event.getNewStatus(); - - switch (newStatus) { - case PENDING: - // 工单已分配,不影响设备状态(设备可能还在处理其他任务) - // 设置设备当前工单 - badgeDeviceStatusService.setCurrentOrder(deviceId, event.getOrderId()); - break; - - case ARRIVED: - // 保洁员已到岗,开始执行任务 - badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY, - event.getOperatorId(), "工单开始执行: " + event.getOrderCode()); - badgeDeviceStatusService.setCurrentOrder(deviceId, event.getOrderId()); - break; - - case PAUSED: - // 任务暂停 - badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.PAUSED, - event.getOperatorId(), event.getRemark()); - break; - - case COMPLETED: - // 任务完成 - badgeDeviceStatusService.clearCurrentOrder(deviceId); - - // 检查是否还有等待任务,如果有则保持 BUSY,否则转为 IDLE - checkAndUpdateDeviceStatusAfterCompletion(deviceId, event); - break; - - case CANCELLED: - // 工单取消 - badgeDeviceStatusService.clearCurrentOrder(deviceId); - - // 检查是否还有等待任务,如果有则保持 BUSY,否则转为 IDLE - checkAndUpdateDeviceStatusAfterCompletion(deviceId, event); - break; - - default: - log.debug("[BadgeDeviceStatusEventHandler] 不处理的状态: {}", newStatus); - break; - } - } - - /** - * 任务完成或取消后,检查并更新设备状态 - *

- * 如果有等待任务,保持 BUSY 状态 - * 如果没有等待任务,转为 IDLE 状态 - */ - private void checkAndUpdateDeviceStatusAfterCompletion(Long deviceId, OrderStateChangedEvent event) { - // 检查是否还有等待任务(通过查询队列) - // 这里简化处理,直接转为 IDLE - // 实际业务中可能需要查询队列判断是否还有等待任务 - - // 检查是否是被P0打断的任务恢复 - boolean isResumeFromInterrupt = event.hasPayload("interruptReason") - && "RESUME_FROM_INTERRUPT".equals(event.getPayloadString("interruptReason")); - - if (isResumeFromInterrupt) { - // 恢复被中断的任务,保持 BUSY - badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY, - event.getOperatorId(), "恢复被中断的任务"); - } else { - // 正常完成,转为 IDLE - badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.IDLE, - event.getOperatorId(), "任务完成,转为空闲"); - } - } - - /** - * 从事件中获取设备ID - *

- * 优先级: - * 1. payload.deviceId - * 2. operatorId(如果 operatorType 是 DEVICE) - * 3. 通过工单查询(需要额外的查询,暂不实现) - */ - private Long getDeviceIdFromEvent(OrderStateChangedEvent event) { - // 优先从 payload 获取 - Long deviceId = event.getPayloadLong("deviceId"); - if (deviceId != null) { - return deviceId; - } - - // TODO: 可以通过工单ID查询获取设备ID - // 这需要注入 OrderService 或 OrderMapper - - return null; - } - - /** - * 处理 P0 紧急任务打断事件 - *

- * 当 P0 任务需要打断当前任务时: - * 1. 将被打断的设备状态转为 PAUSED - * 2. 记录被打断的工单ID - * - * @param deviceId 被打断的设备ID - * @param interruptedOrderId 被打断的工单ID - * @param urgentOrderId P0紧急工单ID - */ - public void handleP0Interrupt(Long deviceId, Long interruptedOrderId, Long urgentOrderId) { - log.info("[BadgeDeviceStatusEventHandler] P0紧急任务打断: deviceId={}, interruptedOrderId={}, urgentOrderId={}", - deviceId, interruptedOrderId, urgentOrderId); - - badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.PAUSED, - null, "被P0紧急任务打断: " + urgentOrderId); - } - - /** - * 处理 P0 任务完成后的恢复事件 - *

- * 当 P0 任务完成后,恢复被打断的任务: - * 1. 将设备状态转为 BUSY - * 2. 设置当前工单为被打断的工单 - * - * @param deviceId 设备ID - * @param interruptedOrderId 被打断的工单ID - */ - public void handleP0Resume(Long deviceId, Long interruptedOrderId) { - log.info("[BadgeDeviceStatusEventHandler] P0任务完成,恢复被中断任务: deviceId={}, interruptedOrderId={}", - deviceId, interruptedOrderId); - - badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY, - null, "P0任务完成,恢复被中断的任务"); - badgeDeviceStatusService.setCurrentOrder(deviceId, interruptedOrderId); - } -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventListener.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventListener.java new file mode 100644 index 0000000..ded725e --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventListener.java @@ -0,0 +1,246 @@ +package com.viewsh.module.ops.environment.integration.listener; + +import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO; +import com.viewsh.module.ops.api.queue.OrderQueueService; +import com.viewsh.module.ops.core.event.OrderStateChangedEvent; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService; +import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * 工牌设备状态事件监听器 + *

+ * 职责:监听工单状态变更事件,同步更新设备状态和工单关联 + *

+ * 设计说明: + * - 使用 BEFORE_COMMIT 阶段,在事务提交前同步执行 + * - 确保 IoT 模块能实时查询到正确的设备工单信息 + * - 只处理保洁类型的工单 + *

+ * 状态处理: + * + * + * + * + * + * + * + * + *
状态设备状态工单关联
DISPATCHEDBUSY设置
CONFIRMED保持BUSY设置
ARRIVED保持BUSY设置完整信息
PAUSEDPAUSED保持
COMPLETED检查等待任务清除
CANCELLEDIDLE清除
+ * + * @author lzh + */ +@Slf4j +@Component +public class BadgeDeviceStatusEventListener { + + @Resource + private OpsOrderMapper opsOrderMapper; + + @Resource + private BadgeDeviceStatusService badgeDeviceStatusService; + + @Resource + private OrderQueueService orderQueueService; + + @Resource + private EventLogRecorder eventLogRecorder; + + /** + * 监听工单状态变更事件,同步更新设备工单关联 + *

+ * 使用 BEFORE_COMMIT 阶段,在事务提交前同步执行 + * 确保 IoT 模块能实时查询到正确的设备工单信息 + */ + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void onOrderStateChanged(OrderStateChangedEvent event) { + try { + // 只处理保洁类型的工单 + if (!"CLEAN".equals(event.getOrderType())) { + return; + } + + WorkOrderStatusEnum newStatus = event.getNewStatus(); + Long orderId = event.getOrderId(); + + log.debug("[BadgeDeviceStatusEventListener] 状态变更: orderId={}, status={}", orderId, newStatus); + + // 查询工单获取设备ID + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.debug("[BadgeDeviceStatusEventListener] 工单不存在,跳过处理: orderId={}", orderId); + return; + } + + // assigneeId 存储的是工牌设备ID + Long deviceId = order.getAssigneeId(); + if (deviceId == null) { + log.debug("[BadgeDeviceStatusEventListener] 工单未关联设备,跳过处理: orderId={}", orderId); + return; + } + + // 根据状态更新设备工单关联 + handleOrderStatusTransition(deviceId, orderId, newStatus, event); + + } catch (Exception e) { + log.error("[BadgeDeviceStatusEventListener] 处理工单状态变更事件失败: orderId={}", event.getOrderId(), e); + } + } + + /** + * 根据工单状态更新设备工牌关联 + */ + private void handleOrderStatusTransition(Long deviceId, Long orderId, WorkOrderStatusEnum newStatus, OrderStateChangedEvent event) { + var waitingTasks = orderQueueService.getWaitingTasksByUserId(deviceId); + switch (newStatus) { + case DISPATCHED: + // 工单已推送到工牌,设置工单关联,设备状态转为 BUSY + badgeDeviceStatusService.setCurrentOrder(deviceId, orderId); + badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY, null, "新工单已推送"); + log.info("[BadgeDeviceStatusEventListener] 工单已推送,设备状态转为 BUSY: deviceId={}, orderId={}", deviceId, orderId); + break; + + case CONFIRMED: + // 设备按键确认,设备保持 BUSY + badgeDeviceStatusService.setCurrentOrder(deviceId, orderId); + log.debug("[BadgeDeviceStatusEventListener] 工单已确认: deviceId={}, orderId={}", deviceId, orderId); + break; + + case ARRIVED: + // 设备已到岗,设置完整的工单信息(areaId, beaconMac) + updateDeviceOrderInfo(deviceId, orderId, event); + recordOrderArrivedLog(orderId, deviceId, event); + log.info("[BadgeDeviceStatusEventListener] 工单已到岗,更新设备工单信息: deviceId={}, orderId={}", deviceId, orderId); + break; + + case PAUSED: + // 检查是否是 P0 打断场景 + Long urgentOrderId = event.getPayloadLong("urgentOrderId"); + if (urgentOrderId != null) { + // P0 打断场景:不修改设备状态,保持当前状态 + // 紧接着会有 P0 工单的 DISPATCHED 事件,将当前工单更新为 P0 工单 + log.info("[BadgeDeviceStatusEventListener] P0打断场景,工单已暂停,等待P0工单派发: pausedOrderId={}, urgentOrderId={}, deviceId={}", + orderId, urgentOrderId, deviceId); + } else { + // 普通暂停场景:设备状态转为 PAUSED + badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.PAUSED, null, "任务暂停"); + log.info("[BadgeDeviceStatusEventListener] 任务暂停,设备状态转为 PAUSED: deviceId={}", deviceId); + } + break; + + case COMPLETED: + // 任务完成,清除工单关联 + badgeDeviceStatusService.clearCurrentOrder(deviceId); + + // 检查是否有等待任务,决定设备状态 + if (waitingTasks != null && !waitingTasks.isEmpty()) { + // 有等待任务,设备状态保持 BUSY(由后续 DISPATCHED 事件更新) + log.info("[BadgeDeviceStatusEventListener] 任务完成,有{}个等待任务,设备保持 BUSY: deviceId={}", + waitingTasks.size(), deviceId); + } else { + // 无等待任务,设备状态转为 IDLE + badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.IDLE, null, "任务完成,无新任务"); + log.info("[BadgeDeviceStatusEventListener] 任务完成,无等待任务,设备转为 IDLE: deviceId={}", deviceId); + } + break; + + case CANCELLED: + // 检查被取消的工单是否是设备当前正在执行的工单 + BadgeDeviceStatusDTO deviceStatus = badgeDeviceStatusService.getBadgeStatus(deviceId); + Long currentOrderId = deviceStatus != null ? deviceStatus.getCurrentOpsOrderId() : null; + + if (orderId.equals(currentOrderId)) { + // 取消的是当前正在执行的工单,清除工单关联 + badgeDeviceStatusService.clearCurrentOrder(deviceId); + + // 检查是否有等待任务,决定设备状态 + if (waitingTasks != null && !waitingTasks.isEmpty()) { + // 有等待任务,设备状态保持 BUSY(由后续 DISPATCHED 事件更新) + log.info("[BadgeDeviceStatusEventListener] 当前工单已取消,有{}个等待任务,设备保持 BUSY: deviceId={}", + waitingTasks.size(), deviceId); + } else { + // 无等待任务,设备状态转为 IDLE + badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.IDLE, null, "工单已取消"); + log.info("[BadgeDeviceStatusEventListener] 当前工单已取消,无等待任务,设备转为 IDLE: deviceId={}", deviceId); + } + } else { + // 取消的不是当前工单(可能是队列中的等待任务),不需要修改设备状态 + log.debug("[BadgeDeviceStatusEventListener] 取消的工单非当前执行工单,跳过设备状态更新: cancelledOrderId={}, currentOrderId={}, deviceId={}", + orderId, currentOrderId, deviceId); + } + break; + + default: + // 其他状态不处理 + break; + } + } + + /** + * 更新设备工单信息(ARRIVED 状态专用) + *

+ * 设置完整的工单信息:工单ID、工单状态、区域ID、信标MAC + */ + private void updateDeviceOrderInfo(Long deviceId, Long orderId, OrderStateChangedEvent event) { + try { + // 从 payload 中提取信息 + Long areaId = event.getPayloadLong("areaId"); + + String beaconMac = null; + Object beaconMacObj = event.getPayload().get("beaconMac"); + if (beaconMacObj != null) { + beaconMac = String.valueOf(beaconMacObj); + } + + // 使用 BadgeDeviceStatusService 设置完整工单信息 + badgeDeviceStatusService.setCurrentOrderInfo( + deviceId, + orderId, + event.getNewStatus().getStatus(), + areaId, + beaconMac + ); + + log.debug("[BadgeDeviceStatusEventListener] 设备工单信息已更新: deviceId={}, orderId={}, areaId={}, beaconMac={}", + deviceId, orderId, areaId, beaconMac); + + } catch (Exception e) { + log.error("[BadgeDeviceStatusEventListener] 更新设备工单信息失败: deviceId={}, orderId={}", deviceId, orderId, e); + } + } + + /** + * 记录工单到岗业务日志 + */ + private void recordOrderArrivedLog(Long orderId, Long deviceId, OrderStateChangedEvent event) { + try { + Long areaId = event.getPayloadLong("areaId"); + String deviceKey = (String) event.getPayload().get("deviceKey"); + + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(EventDomain.BEACON) + .eventType("ORDER_ARRIVED") + .message(String.format("蓝牙信标自动到岗确认 [设备:%s, 区域:%d]", deviceKey, areaId)) + .targetId(orderId) + .targetType("order") + .deviceId(deviceId) + .personId(deviceId) + .build()); + + } catch (Exception e) { + log.warn("[BadgeDeviceStatusEventListener] 记录到岗业务日志失败: orderId={}", orderId, e); + } + } +} + diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java index 2efdbb4..4d03cbb 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java @@ -1,8 +1,7 @@ package com.viewsh.module.ops.environment.integration.listener; -import cn.hutool.core.map.MapUtil; import com.viewsh.module.iot.api.device.IotDeviceControlApi; -import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO; +import com.viewsh.module.iot.api.device.dto.ResetTrafficCounterReqDTO; import com.viewsh.module.ops.core.dispatch.DispatchEngine; import com.viewsh.module.ops.core.dispatch.model.DispatchResult; import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; @@ -15,9 +14,11 @@ import com.viewsh.module.ops.enums.PriorityEnum; import com.viewsh.module.ops.environment.constants.CleanNotificationConstants; import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO; import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper; -import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService; import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService; import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService; +import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; import com.viewsh.module.system.api.notify.NotifyMessageSendApi; import com.viewsh.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import jakarta.annotation.Resource; @@ -28,6 +29,7 @@ import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; import java.time.LocalDateTime; +import java.util.Map; /** * 保洁工单事件监听器(统一入口) @@ -36,12 +38,19 @@ import java.time.LocalDateTime; * 1. 监听工单创建事件,触发自动调度 * 2. 监听工单状态变更事件,处理: * - 扩展表时间记录(到岗、完成、暂停) - * - 保洁员状态同步 - * - 通知发送(语音、震动、站内信) + * - 通知发送(语音、站内信) + * - 业务日志记录 * 3. 监听工单完成事件,触发自动派单下一个任务 *

+ * 职责划分: + * - 设备状态管理:由 BadgeDeviceStatusServiceImpl 处理 + * - 扩展表时间记录:由 CleanOrderEventListener 处理 + * - 通知发送:由 CleanOrderEventListener 处理 + * - 业务日志记录:由 CleanOrderEventListener 处理 + * - 自动调度:由 CleanOrderEventListener 触发 + *

* 设计说明: - * - 整合了 OrderCreatedEventListener、CleanerStateChangeListener、CleanOrderEventHandler 的功能 + * - assigneeId 存储的是工牌设备ID(非保洁员ID) * - 使用异步处理,避免阻塞主流程 * - 使用 @TransactionalEventListener(AFTER_COMMIT) 确保事务提交后再处理 * @@ -60,9 +69,6 @@ public class CleanOrderEventListener { @Resource private OpsOrderCleanExtMapper cleanExtMapper; - @Resource - private CleanerStatusService cleanerStatusService; - @Resource private CleanOrderService cleanOrderService; @@ -75,13 +81,13 @@ public class CleanOrderEventListener { @Resource private IotDeviceControlApi iotDeviceControlApi; + @Resource + private EventLogRecorder eventLogRecorder; + // ==================== 工单创建事件 ==================== /** * 监听工单创建事件,触发自动调度 - *

- * 使用 @TransactionalEventListener 确保在事务提交后才执行调度 - * 避免调度失败导致工单创建回滚 */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { @@ -94,6 +100,11 @@ public class CleanOrderEventListener { // 异步触发调度 asyncDispatchAfterCreated(event); + + // 如果是客流触发的工单,重置客流计数器 + if ("IOT_TRAFFIC".equals(event.getPayload().get("triggerSource"))) { + asyncResetTrafficCounter(event); + } } /** @@ -122,7 +133,7 @@ public class CleanOrderEventListener { DispatchResult result = dispatchEngine.dispatch(context); if (result.isSuccess()) { - log.info("[CleanOrderEventListener] 自动调度成功: orderId={}, assigneeId={}", + log.info("[CleanOrderEventListener] 自动调度成功: orderId={}, deviceId={}", event.getOrderId(), result.getAssigneeId()); } else { log.warn("[CleanOrderEventListener] 自动调度失败: orderId={}, reason={}", @@ -134,10 +145,63 @@ public class CleanOrderEventListener { } } + /** + * 异步重置客流计数器 + */ + @Async("ops-task-executor") + public void asyncResetTrafficCounter(OrderCreatedEvent event) { + try { + Long deviceId = (Long) event.getPayload().get("triggerDeviceId"); + @SuppressWarnings("unchecked") + Map triggerData = (Map) event.getPayload().get("triggerData"); + + if (deviceId == null || triggerData == null) { + log.warn("[CleanOrderEventListener] 缺少客流数据,跳过重置: orderId={}, deviceId={}, triggerData={}", + event.getOrderId(), deviceId, triggerData); + return; + } + + Object actualCountObj = triggerData.get("actualCount"); + if (actualCountObj == null) { + log.warn("[CleanOrderEventListener] 缺少客流值,跳过重置: orderId={}", event.getOrderId()); + return; + } + + // 将当前客流值设为新的基准值 + Long newBaseValue = ((Number) actualCountObj).longValue(); + + // 构建重置请求 + ResetTrafficCounterReqDTO reqDTO = ResetTrafficCounterReqDTO.builder() + .deviceId(deviceId) + .newBaseValue(newBaseValue) + .orderId(event.getOrderId()) + .remark("工单创建后重置计数器") + .build(); + + // 调用 IoT 模块 RPC 接口 + var result = iotDeviceControlApi.resetTrafficCounter(reqDTO); + + if (result.getData() != null && result.getData()) { + log.info("[CleanOrderEventListener] 客流计数器重置成功: orderId={}, deviceId={}, newBaseValue={}", + event.getOrderId(), deviceId, newBaseValue); + } else { + log.warn("[CleanOrderEventListener] 客流计数器重置失败: orderId={}, deviceId={}", + event.getOrderId(), deviceId); + } + + } catch (Exception e) { + // 重置失败不应影响主流程 + log.error("[CleanOrderEventListener] 客流计数器重置异常: orderId={}", event.getOrderId(), e); + } + } + // ==================== 状态变更事件 ==================== /** * 监听状态变更事件 + *

+ * 注意:设备状态由 BadgeDeviceStatusServiceImpl 统一管理 + * 这里只处理扩展表时间记录和通知发送 */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderStateChanged(OrderStateChangedEvent event) { @@ -145,8 +209,8 @@ public class CleanOrderEventListener { return; } - log.info("[CleanOrderEventListener] 状态变更: orderId={}, {} -> {}, operatorId={}", - event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId()); + log.info("[CleanOrderEventListener] 状态变更: orderId={}, {} -> {}", + event.getOrderId(), event.getOldStatus(), event.getNewStatus()); switch (event.getNewStatus()) { case DISPATCHED: @@ -165,7 +229,8 @@ public class CleanOrderEventListener { handleCompleted(event); break; case CANCELLED: - handleCancelled(event); + // 设备状态由 BadgeDeviceStatusEventListener 统一处理 + log.debug("[CleanOrderEventListener] CANCELLED 状态已处理: orderId={}", event.getOrderId()); break; default: break; @@ -177,8 +242,28 @@ public class CleanOrderEventListener { */ @Async("ops-task-executor") public void handleDispatched(OrderStateChangedEvent event) { - // 发送新工单通知(语音+震动+站内信) - sendNewOrderNotification(event.getOperatorId(), event.getOrderId()); + Long orderId = event.getOrderId(); + + // 1. 记录下发时间到扩展表(用于计算响应时长) + recordDispatchedTime(orderId); + + // 2. 如果是从 PAUSED 恢复,处理暂停结束逻辑 + if (event.getOldStatus() == com.viewsh.module.ops.enums.WorkOrderStatusEnum.PAUSED) { + handlePauseEnd(orderId); + } + + // 3. 优先使用 assigneeId(自动调度场景),其次使用 operatorId(手动派单场景) + Long deviceId = event.getPayloadLong("assigneeId"); + if (deviceId == null) { + deviceId = event.getOperatorId(); + } + if (deviceId != null) { + // 发送新工单通知(语音+站内信) + sendNewOrderNotification(deviceId, orderId); + } else { + log.warn("[CleanOrderEventListener] DISPATCHED 事件缺少 assigneeId 和 operatorId: orderId={}", + orderId); + } } /** @@ -186,21 +271,25 @@ public class CleanOrderEventListener { */ @Async("ops-task-executor") public void handleConfirmed(OrderStateChangedEvent event) { - Long cleanerId = event.getOperatorId(); - if (cleanerId == null) { - return; + Long orderId = event.getOrderId(); + + // 获取 deviceId(优先从 payload 获取,其次使用 operatorId) + Long deviceId = event.getPayloadLong("deviceId"); + if (deviceId == null) { + deviceId = event.getOperatorId(); } - // 更新保洁员状态为 BUSY - cleanerStatusService.updateStatus(cleanerId, - com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY, - "确认工单"); + // 记录业务日志 + recordOrderConfirmedLog(orderId, deviceId, event); - // 设置当前工单 - cleanerStatusService.setCurrentWorkOrder(cleanerId, event.getOrderId(), - opsOrderMapper.selectById(event.getOrderId()).getOrderCode()); + if (deviceId != null) { + // 发送确认成功语音播报(使用统一模板) + OpsOrderDO order = opsOrderMapper.selectById(orderId); + String confirmMessage = CleanNotificationConstants.VoiceBuilder.buildOrderConfirmed(order); + playVoice(deviceId, confirmMessage); + } - log.info("[CleanOrderEventListener] 保洁员确认工单,状态更新为BUSY: cleanerId={}", cleanerId); + log.info("[CleanOrderEventListener] 工单已确认: orderId={}, deviceId={}", orderId, deviceId); } /** @@ -216,35 +305,28 @@ public class CleanOrderEventListener { updateObj.setArrivedTime(LocalDateTime.now()); cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); - log.info("[CleanOrderEventListener] 保洁员已到岗: orderId={}", orderId); + // 2. 计算并更新响应时长(下发→到岗,排除暂停时间) + updateResponseSeconds(orderId); + + log.info("[CleanOrderEventListener] 到岗时间已记录: orderId={}", orderId); } /** * 处理暂停状态 + *

+ * 注意:PAUSED 状态不是由 IoT 消息触发的,而是通过服务层 (REST API) 调用: + * - OrderLifecycleManager.pauseOrder() - 用户主动暂停 + * - OrderLifecycleManager.interruptOrder() - P0任务打断 */ @Async("ops-task-executor") public void handlePaused(OrderStateChangedEvent event) { Long orderId = event.getOrderId(); - Long operatorId = event.getOperatorId(); - if (operatorId == null) { - return; - } - // 检查是否是被 P0 任务打断 - String interruptReason = event.getPayloadString("interruptReason"); - if ("P0_TASK_INTERRUPT".equals(interruptReason)) { - log.warn("[CleanOrderEventListener] 保洁任务被P0任务打断: orderId={}", orderId); - // 释放保洁员资源 - cleanerStatusService.clearCurrentWorkOrder(operatorId); - } else { - // 普通暂停:更新保洁员状态为 PAUSED - cleanerStatusService.updateStatus(operatorId, - com.viewsh.module.ops.enums.CleanerStatusEnum.PAUSED, - event.getRemark() != null ? event.getRemark() : "任务暂停"); - } - - // 记录暂停开始时间 + // 记录暂停开始时间到扩展表 recordPauseStartTime(orderId); + + // 设备状态由 BadgeDeviceStatusEventListener 统一处理 + log.info("[CleanOrderEventListener] 暂停时间已记录: orderId={}", orderId); } /** @@ -254,35 +336,27 @@ public class CleanOrderEventListener { public void handleCompleted(OrderStateChangedEvent event) { Long orderId = event.getOrderId(); - // 1. 计算作业时长 - Integer actualDuration = cleanOrderService.calculateActualDuration(orderId); + // 获取 deviceId(优先从 payload 获取,其次从工单获取) + Long deviceId = event.getPayloadLong("deviceId"); - // 2. 记录完成时间到扩展表 + // 记录完成时间到扩展表 OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); updateObj.setOpsOrderId(orderId); updateObj.setCompletedTime(LocalDateTime.now()); cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); - log.info("[CleanOrderEventListener] 保洁作业完成: orderId={}, actualDuration={}秒", orderId, actualDuration); - } + // 记录业务日志 + recordOrderCompletedLog(orderId, deviceId, event); - /** - * 处理取消状态 - */ - @Async("ops-task-executor") - public void handleCancelled(OrderStateChangedEvent event) { - Long operatorId = event.getOperatorId(); - if (operatorId != null) { - // 清理保洁员当前工单 - cleanerStatusService.clearCurrentWorkOrder(operatorId); - } - log.info("[CleanOrderEventListener] 保洁工单已取消: orderId={}", event.getOrderId()); + log.info("[CleanOrderEventListener] 完成时间已记录: orderId={}, deviceId={}", orderId, deviceId); } // ==================== 工单完成事件 ==================== /** * 监听工单完成事件 + *

+ * 触发自动调度下一个任务 */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCompleted(OrderCompletedEvent event) { @@ -290,19 +364,16 @@ public class CleanOrderEventListener { return; } - log.info("[CleanOrderEventListener] 工单完成: orderId={}, assigneeId={}", + log.info("[CleanOrderEventListener] 工单完成: orderId={}, deviceId={}", event.getOrderId(), event.getAssigneeId()); - Long assigneeId = event.getAssigneeId(); - if (assigneeId != null) { - // 1. 清理当前工单 - cleanerStatusService.clearCurrentWorkOrder(assigneeId); - - // 2. 自动推送下一个任务(异步) - asyncDispatchNext(assigneeId, event.getOrderId()); + Long deviceId = event.getAssigneeId(); + if (deviceId != null) { + // 自动推送下一个任务(异步) + asyncDispatchNext(deviceId, event.getOrderId()); } - // 3. 发送完成通知(异步) + // 发送完成通知(异步) asyncSendOrderCompletedNotification(event.getOrderId()); } @@ -310,8 +381,8 @@ public class CleanOrderEventListener { * 异步推送下一个任务 */ @Async("ops-task-executor") - public void asyncDispatchNext(Long cleanerId, Long completedOrderId) { - cleanOrderService.autoDispatchNextOrder(completedOrderId, cleanerId); + public void asyncDispatchNext(Long deviceId, Long completedOrderId) { + cleanOrderService.autoDispatchNextOrder(completedOrderId, deviceId); } /** @@ -325,10 +396,10 @@ public class CleanOrderEventListener { // ==================== 通知方法 ==================== /** - * 发送新工单通知(语音播报 + 震动提醒 + 站内信) + * 发送新工单通知(语音播报 + 站内信) */ @Async("ops-task-executor") - public void sendNewOrderNotification(Long cleanerId, Long orderId) { + public void sendNewOrderNotification(Long deviceId, Long orderId) { try { OpsOrderDO order = opsOrderMapper.selectById(orderId); if (order == null) { @@ -336,25 +407,23 @@ public class CleanOrderEventListener { return; } - log.info("[新工单通知] cleanerId={}, orderId={}", cleanerId, orderId); + log.info("[新工单通知] deviceId={}, orderId={}", deviceId, orderId); - // 1. 语音播报 - playVoice(cleanerId, CleanNotificationConstants.VoiceMessage.NEW_ORDER); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildNewOrder(order, true); + playVoice(deviceId, voiceMessage); - // 2. 震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, + // 2. 发送站内信(暂时发送到管理员) + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.NEW_ORDER, CleanNotificationConstants.NotifyParamsBuilder.newOrderParams( order.getOrderCode(), order.getTitle(), - getAreaName(order.getAreaId()) + CleanNotificationConstants.VoiceBuilder.getAreaName(order.getLocation()) )); } catch (Exception e) { - log.error("[新工单通知] 发送失败: cleanerId={}, orderId={}", cleanerId, orderId, e); + log.error("[新工单通知] 发送失败: deviceId={}, orderId={}", deviceId, orderId, e); } } @@ -362,26 +431,23 @@ public class CleanOrderEventListener { * 发送待办增加通知 */ @Async("ops-task-executor") - public void sendQueuedOrderNotification(Long cleanerId, int queueCount) { + public void sendQueuedOrderNotification(Long deviceId, int queueCount) { try { - log.info("[待办增加通知] cleanerId={}, queueCount={}", cleanerId, queueCount); + log.info("[待办增加通知] deviceId={}, queueCount={}", deviceId, queueCount); - // 1. 语音播报 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatQueuedOrder(queueCount, 1); - playVoice(cleanerId, voiceMessage); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildQueuedOrder(queueCount); + playVoice(deviceId, voiceMessage); - // 2. 震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.LIGHT); - - // 3. 发送站内信(待办数量较多时) + // 2. 发送站内信(待办数量较多时) if (queueCount >= 3) { - sendNotifyMessageToMember(cleanerId, + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.QUEUED_ORDER, CleanNotificationConstants.NotifyParamsBuilder.queuedOrderParams(queueCount, 1)); } } catch (Exception e) { - log.error("[待办增加通知] 发送失败: cleanerId={}", cleanerId, e); + log.error("[待办增加通知] 发送失败: deviceId={}", deviceId, e); } } @@ -389,24 +455,21 @@ public class CleanOrderEventListener { * 发送下一个任务通知 */ @Async("ops-task-executor") - public void sendNextTaskNotification(Long cleanerId, int queueCount, String orderTitle) { + public void sendNextTaskNotification(Long deviceId, int queueCount, String orderTitle) { try { - log.info("[下一任务通知] cleanerId={}, queueCount={}, title={}", cleanerId, queueCount, orderTitle); + log.info("[下一任务通知] deviceId={}, queueCount={}, title={}", deviceId, queueCount, orderTitle); - // 1. 语音播报 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatNextTask(queueCount, orderTitle); - playVoice(cleanerId, voiceMessage); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildNextTask(orderTitle); + playVoice(deviceId, voiceMessage); - // 2. 震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, + // 2. 发送站内信 + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.NEXT_TASK, CleanNotificationConstants.NotifyParamsBuilder.nextTaskParams(queueCount, orderTitle)); } catch (Exception e) { - log.error("[下一任务通知] 发送失败: cleanerId={}", cleanerId, e); + log.error("[下一任务通知] 发送失败: deviceId={}", deviceId, e); } } @@ -414,24 +477,21 @@ public class CleanOrderEventListener { * 发送P0紧急任务插队通知 */ @Async("ops-task-executor") - public void sendPriorityUpgradeNotification(Long cleanerId, String orderCode) { + public void sendPriorityUpgradeNotification(Long deviceId, String orderCode) { try { - log.warn("[P0紧急通知] cleanerId={}, orderCode={}", cleanerId, orderCode); + log.warn("[P0紧急通知] deviceId={}, orderCode={}", deviceId, orderCode); - // 1. 语音播报 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatPriorityUpgrade(orderCode); - playVoice(cleanerId, voiceMessage); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildPriorityUpgrade(orderCode); + playVoice(deviceId, voiceMessage); - // 2. 强烈震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.STRONG); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, + // 2. 发送站内信 + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.PRIORITY_UPGRADE, CleanNotificationConstants.NotifyParamsBuilder.priorityUpgradeParams(orderCode, "P0紧急任务")); } catch (Exception e) { - log.error("[P0紧急通知] 发送失败: cleanerId={}", cleanerId, e); + log.error("[P0紧急通知] 发送失败: deviceId={}", deviceId, e); } } @@ -439,24 +499,21 @@ public class CleanOrderEventListener { * 发送任务恢复通知 */ @Async("ops-task-executor") - public void sendTaskResumedNotification(Long cleanerId, String areaName) { + public void sendTaskResumedNotification(Long deviceId, String areaName) { try { - log.info("[任务恢复通知] cleanerId={}, areaName={}", cleanerId, areaName); + log.info("[任务恢复通知] deviceId={}, areaName={}", deviceId, areaName); - // 1. 语音播报 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatTaskResumed(areaName); - playVoice(cleanerId, voiceMessage); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildTaskResumed(areaName); + playVoice(deviceId, voiceMessage); - // 2. 震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, + // 2. 发送站内信 + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.TASK_RESUMED, CleanNotificationConstants.NotifyParamsBuilder.taskResumedParams(areaName)); } catch (Exception e) { - log.error("[任务恢复通知] 发送失败: cleanerId={}", cleanerId, e); + log.error("[任务恢复通知] 发送失败: deviceId={}", deviceId, e); } } @@ -485,73 +542,31 @@ public class CleanOrderEventListener { * 播放语音(供外部调用) */ @Async("ops-task-executor") - public void playVoiceForNewOrder(Long cleanerId) { - playVoice(cleanerId, CleanNotificationConstants.VoiceMessage.NEW_ORDER); + public void playVoiceForNewOrder(Long deviceId) { + playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.NEW_ORDER_SHORT); } - // ==================== IoT 设备操作���法 ==================== + // ==================== 设备操作方法 ==================== /** * 语音播报 */ - private void playVoice(Long cleanerId, String message) { + private void playVoice(Long deviceId, String message) { try { - Long deviceId = getBadgeDeviceId(cleanerId); - if (deviceId == null) { - log.warn("[语音播报] 保洁员无关联工牌设备: cleanerId={}", cleanerId); - return; - } - voiceBroadcastService.broadcast(deviceId, message); - log.debug("[语音播报] 调用成功: cleanerId={}, deviceId={}, message={}", cleanerId, deviceId, message); + log.debug("[语音播报] 调用成功: deviceId={}, message={}", deviceId, message); } catch (Exception e) { - log.error("[语音播报] 调用失败: cleanerId={}, message={}", cleanerId, message, e); + log.error("[语音播报] 调用失败: deviceId={}, message={}", deviceId, message, e); } } - /** - * 震动提醒 - */ - private void vibrate(Long cleanerId, int durationMs) { - try { - Long deviceId = getBadgeDeviceId(cleanerId); - if (deviceId == null) { - return; - } - - IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO(); - reqDTO.setDeviceId(deviceId); - reqDTO.setIdentifier("vibrate"); - reqDTO.setParams(MapUtil.builder() - .put("duration", durationMs) - .put("intensity", 50) - .build()); - - iotDeviceControlApi.invokeService(reqDTO); - log.debug("[震动提醒] 调用成功: cleanerId={}, durationMs={}", cleanerId, durationMs); - - } catch (Exception e) { - log.error("[震动提醒] 调用失败: cleanerId={}, durationMs={}", cleanerId, durationMs, e); - } - } - - /** - * 获取保洁员关联的工牌设备ID - * TODO: 需要实现 - */ - private Long getBadgeDeviceId(Long cleanerId) { - // TODO: 实现获取保洁员关联的工牌设备ID - return null; - } - // ==================== 站内信发送方法 ==================== /** - * 发送站内信给保洁员 + * 发送站内信 */ - private void sendNotifyMessageToMember(Long userId, String templateCode, - java.util.Map templateParams) { + private void sendNotifyMessage(Long userId, String templateCode, Map templateParams) { try { NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO(); reqDTO.setUserId(userId); @@ -566,25 +581,6 @@ public class CleanOrderEventListener { } } - /** - * 发送站内信给管理员 - */ - private void sendNotifyMessageToAdmin(Long userId, String templateCode, - java.util.Map templateParams) { - try { - NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO(); - reqDTO.setUserId(userId); - reqDTO.setTemplateCode(templateCode); - reqDTO.setTemplateParams(templateParams); - - notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO); - log.debug("[站内信发送成功] userId={}, templateCode={}", userId, templateCode); - - } catch (Exception e) { - log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e); - } - } - // ==================== 辅助方法 ==================== /** @@ -604,4 +600,167 @@ public class CleanOrderEventListener { updateObj.setPauseStartTime(LocalDateTime.now()); cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); } + + /** + * 记录下发时间 + */ + private void recordDispatchedTime(Long orderId) { + // 先查询现有记录 + OpsOrderCleanExtDO existing = cleanExtMapper.selectByOpsOrderId(orderId); + LocalDateTime now = LocalDateTime.now(); + + if (existing == null || existing.getFirstDispatchedTime() == null) { + // 首次下发,记录首次下发时间 + OpsOrderCleanExtDO updateObj = OpsOrderCleanExtDO.builder() + .opsOrderId(orderId) + .dispatchedTime(now) + .firstDispatchedTime(now) + .build(); + cleanExtMapper.insertOrUpdateSelective(updateObj); + log.info("[CleanOrderEventListener] 首次下发时间已记录: orderId={}", orderId); + } else { + // 非首次下发,只更新最近下发时间 + OpsOrderCleanExtDO updateObj = OpsOrderCleanExtDO.builder() + .opsOrderId(orderId) + .dispatchedTime(now) + .firstDispatchedTime(existing.getFirstDispatchedTime()) // 保留首次时间 + .build(); + cleanExtMapper.insertOrUpdateSelective(updateObj); + log.debug("[CleanOrderEventListener] 下发时间已更新: orderId={}", orderId); + } + } + + /** + * 处理暂停结束 + *

+ * 计算暂停时长并累加到 totalPauseSeconds + */ + private void handlePauseEnd(Long orderId) { + try { + OpsOrderCleanExtDO ext = cleanExtMapper.selectByOpsOrderId(orderId); + if (ext == null || ext.getPauseStartTime() == null) { + log.warn("[CleanOrderEventListener] 暂停开始时间不存在,无法计算暂停时长: orderId={}", orderId); + return; + } + + // 计算暂停时长(秒) + LocalDateTime pauseEnd = LocalDateTime.now(); + long pauseSeconds = java.time.Duration.between(ext.getPauseStartTime(), pauseEnd).getSeconds(); + + // 获取现有累计暂停时长 + int totalPauseSeconds = ext.getTotalPauseSeconds() != null ? ext.getTotalPauseSeconds() : 0; + totalPauseSeconds += (int) pauseSeconds; + + // 更新暂停结束时间和累计暂停时长 + OpsOrderCleanExtDO updateObj = OpsOrderCleanExtDO.builder() + .opsOrderId(orderId) + .pauseEndTime(pauseEnd) + .totalPauseSeconds(totalPauseSeconds) + .firstDispatchedTime(ext.getFirstDispatchedTime()) // 保留首次下发时间 + .build(); + cleanExtMapper.insertOrUpdateSelective(updateObj); + + log.info("[CleanOrderEventListener] 暂停时长已累加: orderId={}, 本次={}秒, 累计={}秒", + orderId, pauseSeconds, totalPauseSeconds); + + } catch (Exception e) { + log.error("[CleanOrderEventListener] 处理暂停结束失败: orderId={}", orderId, e); + } + } + + /** + * 计算并更新响应时长 + *

+ * 响应时长 = 到岗时间 - 首次下发时间 - 累计暂停时长 + */ + private void updateResponseSeconds(Long orderId) { + try { + OpsOrderCleanExtDO ext = cleanExtMapper.selectByOpsOrderId(orderId); + if (ext == null || ext.getFirstDispatchedTime() == null || ext.getArrivedTime() == null) { + log.warn("[CleanOrderEventListener] 缺少必要时间数据,无法计算响应时长: orderId={}", orderId); + return; + } + + // 计算响应时长(秒):到岗 - 首次下发 - 暂停时长 + long responseSeconds = java.time.Duration.between(ext.getFirstDispatchedTime(), ext.getArrivedTime()).getSeconds(); + int totalPauseSeconds = ext.getTotalPauseSeconds() != null ? ext.getTotalPauseSeconds() : 0; + responseSeconds = responseSeconds - totalPauseSeconds; + + // 确保不为负数 + responseSeconds = Math.max(responseSeconds, 0); + + // 更新工单主表的响应时长 + OpsOrderDO orderUpdate = new OpsOrderDO(); + orderUpdate.setId(orderId); + orderUpdate.setResponseSeconds((int) responseSeconds); + opsOrderMapper.updateById(orderUpdate); + + log.info("[CleanOrderEventListener] 响应时长已更新: orderId={}, 响应时长={}秒", orderId, responseSeconds); + + } catch (Exception e) { + log.error("[CleanOrderEventListener] 更新响应时长失败: orderId={}", orderId, e); + } + } + + /** + * 记录工单确认业务日志 + */ + private void recordOrderConfirmedLog(Long orderId, Long deviceId, OrderStateChangedEvent event) { + try { + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(EventDomain.DEVICE) + .eventType("ORDER_CONFIRM") + .message("工单已确认 (工牌按键)") + .targetId(orderId) + .targetType("order") + .deviceId(deviceId) + .personId(deviceId) + .build()); + + } catch (Exception e) { + log.warn("[CleanOrderEventListener] 记录确认业务日志失败: orderId={}", orderId, e); + } + } + + /** + * 记录工单完成业务日志 + */ + private void recordOrderCompletedLog(Long orderId, Long deviceId, OrderStateChangedEvent event) { + try { + // 从 payload 中提取完成原因和作业时长 + String triggerSource = (String) event.getPayload().get("triggerSource"); + String deviceKey = (String) event.getPayload().get("deviceKey"); + + // 构建日志消息 + String message = "工单已完成"; + if ("SIGNAL_LOSS_TIMEOUT".equals(triggerSource)) { + Object durationMs = event.getPayload().get("durationMs"); + String durationInfo = ""; + if (durationMs != null) { + long durationMinutes = ((Number) durationMs).longValue() / 60000; + durationInfo = String.format(",作业时长: %d分钟", durationMinutes); + } + message = "信号丢失超时自动完成 [设备:" + deviceKey + durationInfo + "]"; + } + + EventLogRecord.EventLogRecordBuilder builder = EventLogRecord.builder() + .module("clean") + .domain(EventDomain.BEACON) + .eventType("ORDER_COMPLETED") + .message(message) + .targetId(orderId) + .targetType("order"); + + if (deviceId != null) { + builder.deviceId(deviceId).personId(deviceId); + } + + eventLogRecorder.record(builder.build()); + + } catch (Exception e) { + log.warn("[CleanOrderEventListener] 记录完成业务日志失败: orderId={}", orderId, e); + } + } } + diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java index 8403523..ea95170 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java @@ -19,6 +19,16 @@ import java.util.stream.Collectors; * 工牌设备状态服务实现 *

* 基于 Redis Hash 存储设备状态,Set 维护区域设备索引 + *

+ * 职责: + * 1. 设备状态管理(IDLE/BUSY/PAUSED/OFFLINE) + * 2. 设备与工单关联管理 + * 3. 区域设备索引管理 + * 4. 心跳超时检查 + *

+ * 设计说明: + * - 状态变更事件由 {@link com.viewsh.module.ops.environment.integration.listener.BadgeDeviceStatusEventListener} 处理 + * - 本类只提供基础的服务方法 * * @author lzh */ diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderService.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderService.java index 9221ec6..aa49d75 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderService.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderService.java @@ -59,24 +59,24 @@ public interface CleanOrderService { // ========== 保洁特有的状态转换 ========== /** - * 保洁员按键确认工单 + * 设备按键确认工单 * 状态转换:DISPATCHED → CONFIRMED - * 同时更新保洁员状态:IDLE → BUSY(关键:确认后才变为忙碌) + * 设备状态保持 BUSY(在 DISPATCHED 时已设置) * - * @param orderId 工单ID - * @param cleanerId 保洁员ID + * @param orderId 工单ID + * @param deviceId 工牌设备ID */ - void confirmOrder(Long orderId, Long cleanerId); + void confirmOrder(Long orderId, Long deviceId); /** * 感知信标,开始作业 * 状态转换:CONFIRMED → ARRIVED * - * @param orderId 工单ID - * @param cleanerId 保洁员ID - * @param beaconId 信标ID + * @param orderId 工单ID + * @param deviceId 工牌设备ID + * @param beaconId 信标ID */ - void startWorkingOnBeacon(Long orderId, Long cleanerId, Long beaconId); + void startWorkingOnBeacon(Long orderId, Long deviceId, Long beaconId); /** * 丢失信号,自动完成 @@ -84,11 +84,11 @@ public interface CleanOrderService { * 完成后自动推送队列中的下一个任务 * * @param orderId 工单ID - * @param cleanerId 保洁员ID - * @param beaconId 信标ID - * @param lostSeconds 丢失信号时长(秒) + * @param deviceId 工牌设备ID + * @param beaconId 信标ID + * @param lostSeconds 丢失信号时长(秒) */ - void autoCompleteOnSignalLost(Long orderId, Long cleanerId, Long beaconId, Integer lostSeconds); + void autoCompleteOnSignalLost(Long orderId, Long deviceId, Long beaconId, Integer lostSeconds); // ========== 任务自动切换 ========== @@ -96,37 +96,38 @@ public interface CleanOrderService { * 任务完成后,自动推送队列中的下一个任务 * * @param completedOrderId 已完成的工单ID - * @param cleanerId 保洁员ID + * @param deviceId 工牌设备ID */ - void autoDispatchNextOrder(Long completedOrderId, Long cleanerId); + void autoDispatchNextOrder(Long completedOrderId, Long deviceId); + // ========== 语音播报(支持去重合并) ========== /** - * 语音播报:空闲保洁员收到新工单 + * 语音播报:空闲设备收到新工单 * - * @param cleanerId 保洁员ID + * @param deviceId 工牌设备ID */ - void playVoiceForNewOrder(Long cleanerId); + void playVoiceForNewOrder(Long deviceId); /** - * 语音播报:忙碌保洁员收到待办(支持去重合并) + * 语音播报:忙碌设备收到待办(支持去重合并) * 例如:"新增3项待办,您共有5个待办工单" * - * @param cleanerId 保洁员ID + * @param deviceId 工牌设备ID * @param queueCount 当前待办数量 */ - void playVoiceForQueuedOrder(Long cleanerId, int queueCount); + void playVoiceForQueuedOrder(Long deviceId, int queueCount); /** * 语音播报:任务完成后自动推送下一个 * 例如:"待办工单总数2个,第一位待办工单xxx" * - * @param cleanerId 保洁员ID + * @param deviceId 工牌设备ID * @param queueCount 待办数量 * @param nextTaskTitle 下一个任务标题 */ - void playVoiceForNextTask(Long cleanerId, int queueCount, String nextTaskTitle); + void playVoiceForNextTask(Long deviceId, int queueCount, String nextTaskTitle); // ========== 优先级管理 ========== diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderServiceImpl.java index 947c056..cec7bec 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderServiceImpl.java @@ -4,7 +4,6 @@ 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.DispatchResult; -import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; import com.viewsh.module.ops.core.event.OrderCreatedEvent; import com.viewsh.module.ops.core.event.OrderEventPublisher; import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager; @@ -120,7 +119,7 @@ public class CleanOrderServiceImpl implements CleanOrderService { log.info("创建自动保洁工单成功: orderId={}, orderCode={}, expectedDuration={}分钟, triggerSource={}", orderId, orderCode, createReq.getExpectedDuration(), createReq.getTriggerSource()); - // 5. 发布工单创建事件,由 CleanOrderEventListener 触发调度 + // 5. 发布工单创建事件 OrderCreatedEvent event = OrderCreatedEvent.builder() .orderId(orderId) .orderType("CLEAN") @@ -132,7 +131,14 @@ public class CleanOrderServiceImpl implements CleanOrderService { .build() .addPayload("isAuto", true) .addPayload("expectedDuration", createReq.getExpectedDuration()) - .addPayload("triggerSource", createReq.getTriggerSource()); + .addPayload("triggerSource", createReq.getTriggerSource()) + .addPayload("triggerDeviceId", createReq.getTriggerDeviceId()) + .addPayload("triggerDeviceKey", createReq.getTriggerDeviceKey()); + + // 添加触发数据(用于客流计数器重置等后续处理) + if (createReq.getTriggerData() != null && !createReq.getTriggerData().isEmpty()) { + event.getPayload().put("triggerData", createReq.getTriggerData()); + } orderEventPublisher.publishOrderCreated(event); @@ -143,28 +149,28 @@ public class CleanOrderServiceImpl implements CleanOrderService { @Override @Transactional(rollbackFor = Exception.class) - public void enqueueOrderOnly(Long orderId, Long cleanerId) { + public void enqueueOrderOnly(Long orderId, Long deviceId) { // 使用生命周期管理器入队 OrderTransitionRequest request = OrderTransitionRequest.builder() .orderId(orderId) - .assigneeId(cleanerId) + .assigneeId(deviceId) .operatorType(OperatorTypeEnum.SYSTEM) - .operatorId(cleanerId) + .operatorId(deviceId) .reason("执行人忙碌,任务入队") .build(); orderLifecycleManager.enqueue(request); - log.info("工单已入队: orderId={}, cleanerId={}", orderId, cleanerId); + log.info("工单已入队: orderId={}, deviceId={}", orderId, deviceId); } @Override @Transactional(rollbackFor = Exception.class) - public void enqueueAndDispatch(Long orderId, Long cleanerId) { - enqueueAndDispatch(orderId, cleanerId, false); + public void enqueueAndDispatch(Long orderId, Long deviceId) { + enqueueAndDispatch(orderId, deviceId, false); } @Transactional(rollbackFor = Exception.class) - public void enqueueAndDispatch(Long orderId, Long cleanerId, boolean isUrgent) { + public void enqueueAndDispatch(Long orderId, Long deviceId, boolean isUrgent) { // 查询工单 OpsOrderDO order = opsOrderMapper.selectById(orderId); if (order == null) { @@ -174,25 +180,25 @@ public class CleanOrderServiceImpl implements CleanOrderService { if (isUrgent) { // P0紧急任务:使用紧急插队 - DispatchResult result = dispatchEngine.urgentInterrupt(orderId, cleanerId); + DispatchResult result = dispatchEngine.urgentInterrupt(orderId, deviceId); log.warn("P0紧急派单结果: orderId={}, success={}", orderId, result.isSuccess()); } else { // 普通任务:正常派单 OrderTransitionRequest request = OrderTransitionRequest.builder() .orderId(orderId) .targetStatus(WorkOrderStatusEnum.DISPATCHED) - .assigneeId(cleanerId) + .assigneeId(deviceId) .operatorType(OperatorTypeEnum.SYSTEM) - .operatorId(cleanerId) + .operatorId(deviceId) .reason("自动派单") .build(); orderLifecycleManager.dispatch(request); - log.info("工单已派发: orderId={}, cleanerId={}", orderId, cleanerId); + log.info("工单已派发: orderId={}, deviceId={}", orderId, deviceId); } // 语音播报 - cleanOrderEventListener.sendNewOrderNotification(cleanerId, orderId); + cleanOrderEventListener.sendNewOrderNotification(deviceId, orderId); } @Override @@ -266,35 +272,35 @@ public class CleanOrderServiceImpl implements CleanOrderService { @Override @Transactional(rollbackFor = Exception.class) - public void confirmOrder(Long orderId, Long cleanerId) { - log.info("保洁员确认工单: orderId={}, cleanerId={}", orderId, cleanerId); + public void confirmOrder(Long orderId, Long deviceId) { + log.info("设备确认工单: orderId={}, deviceId={}", orderId, deviceId); // 使用生命周期管理器转换状态 OrderTransitionRequest request = OrderTransitionRequest.builder() .orderId(orderId) .targetStatus(WorkOrderStatusEnum.CONFIRMED) - .assigneeId(cleanerId) + .assigneeId(deviceId) .operatorType(OperatorTypeEnum.CLEANER) - .operatorId(cleanerId) + .operatorId(deviceId) .reason("确认工单") .build(); orderLifecycleManager.transition(request); - // 注意:保洁员状态更新由 CleanOrderEventListener 处理 + // 注意:设备状态更新由 BadgeDeviceStatusServiceImpl 处理 } @Override @Transactional(rollbackFor = Exception.class) - public void startWorkingOnBeacon(Long orderId, Long cleanerId, Long beaconId) { - log.info("感知信标,开始作业: orderId={}, cleanerId={}, beaconId={}", orderId, cleanerId, beaconId); + public void startWorkingOnBeacon(Long orderId, Long deviceId, Long beaconId) { + log.info("感知信标,开始作业: orderId={}, deviceId={}, beaconId={}", orderId, deviceId, beaconId); // 使用生命周期管理器转换状态 OrderTransitionRequest request = OrderTransitionRequest.builder() .orderId(orderId) .targetStatus(WorkOrderStatusEnum.ARRIVED) - .assigneeId(cleanerId) + .assigneeId(deviceId) .operatorType(OperatorTypeEnum.SYSTEM) - .operatorId(cleanerId) + .operatorId(deviceId) .reason("感知信标,开始作业") .build(); request.putPayload("beaconId", beaconId); @@ -307,17 +313,17 @@ public class CleanOrderServiceImpl implements CleanOrderService { @Override @Transactional(rollbackFor = Exception.class) - public void autoCompleteOnSignalLost(Long orderId, Long cleanerId, Long beaconId, Integer lostSeconds) { - log.info("丢失信号,自动完成工单: orderId={}, cleanerId={}, lostSeconds={}秒", - orderId, cleanerId, lostSeconds); + public void autoCompleteOnSignalLost(Long orderId, Long deviceId, Long beaconId, Integer lostSeconds) { + log.info("丢失信号,自动完成工单: orderId={}, deviceId={}, lostSeconds={}秒", + orderId, deviceId, lostSeconds); // 使用生命周期管理器转换状态 OrderTransitionRequest request = OrderTransitionRequest.builder() .orderId(orderId) .targetStatus(WorkOrderStatusEnum.COMPLETED) - .assigneeId(cleanerId) + .assigneeId(deviceId) .operatorType(OperatorTypeEnum.SYSTEM) - .operatorId(cleanerId) + .operatorId(deviceId) .reason("丢失信号,自动完成") .build(); request.putPayload("lostSeconds", lostSeconds); @@ -329,40 +335,39 @@ public class CleanOrderServiceImpl implements CleanOrderService { recordCompletedTime(orderId); // 自动推送下一个任务 - dispatchEngine.autoDispatchNext(orderId, cleanerId); + dispatchEngine.autoDispatchNext(orderId, deviceId); } @Override @Transactional(rollbackFor = Exception.class) - public void autoDispatchNextOrder(Long completedOrderId, Long cleanerId) { - log.info("任务完成后自动调度下一个: completedOrderId={}, cleanerId={}", completedOrderId, cleanerId); + public void autoDispatchNextOrder(Long completedOrderId, Long deviceId) { + log.info("任务完成后自动调度下一个: completedOrderId={}, deviceId={}", completedOrderId, deviceId); - // 使用新的调度引擎自动推送下一个 - DispatchResult result = dispatchEngine.autoDispatchNext(completedOrderId, cleanerId); + // 使用调度引擎自动推送下一个 + DispatchResult result = dispatchEngine.autoDispatchNext(completedOrderId, deviceId); if (result.isSuccess()) { - log.info("已自动推送下一个任务: cleanerId={}", cleanerId); + log.info("已自动推送下一个任务: deviceId={}", deviceId); } else { - log.info("无等待任务,保洁员变空闲: cleanerId={}", cleanerId); - // 状态更新由 CleanOrderEventListener 处理 + log.info("无等待任务,设备变空闲: deviceId={}", deviceId); } } // ==================== 语音播报(委托给 EventHandler)==================== @Override - public void playVoiceForNewOrder(Long cleanerId) { - cleanOrderEventListener.sendNewOrderNotification(cleanerId, null); + public void playVoiceForNewOrder(Long deviceId) { + cleanOrderEventListener.sendNewOrderNotification(deviceId, null); } @Override - public void playVoiceForQueuedOrder(Long cleanerId, int queueCount) { - cleanOrderEventListener.sendQueuedOrderNotification(cleanerId, queueCount); + public void playVoiceForQueuedOrder(Long deviceId, int queueCount) { + cleanOrderEventListener.sendQueuedOrderNotification(deviceId, queueCount); } @Override - public void playVoiceForNextTask(Long cleanerId, int queueCount, String nextTaskTitle) { - cleanOrderEventListener.sendNextTaskNotification(cleanerId, queueCount, nextTaskTitle); + public void playVoiceForNextTask(Long deviceId, int queueCount, String nextTaskTitle) { + cleanOrderEventListener.sendNextTaskNotification(deviceId, queueCount, nextTaskTitle); } // ==================== 作业时长计算 ==================== diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderEndToEndTest.java b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderEndToEndTest.java index c1949f5..52dd069 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderEndToEndTest.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderEndToEndTest.java @@ -19,6 +19,7 @@ import com.viewsh.framework.common.pojo.CommonResult; import com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener; import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderServiceImpl; import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService; +import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService; import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator; import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator; import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; @@ -93,6 +94,10 @@ public class CleanOrderEndToEndTest { private ValueOperations valueOperations; @Mock private VoiceBroadcastService voiceBroadcastService; + + @Mock + private BadgeDeviceStatusService badgeDeviceStatusService; + @Mock private EventLogRecorder eventLogRecorder; @@ -256,7 +261,8 @@ public class CleanOrderEndToEndTest { @Test void testAV01_BeaconAutoArrive() throws Exception { injectField(arriveEventHandler, "orderLifecycleManager", orderLifecycleManager); - + injectField(arriveEventHandler, "badgeDeviceStatusService", badgeDeviceStatusService); + // 准备工单:状态 DISPATCHED Long orderId = 1001L; OpsOrderDO order = OpsOrderDO.builder() @@ -294,9 +300,10 @@ public class CleanOrderEndToEndTest { assertEquals(orderId, req.getOrderId()); assertEquals(WorkOrderStatusEnum.ARRIVED, req.getTargetStatus()); assertTrue(req.getReason().contains("自动到岗确认")); - - // 3. 验证 Redis 缓存更新 - verify(valueOperations).set(contains("ops:clean:device:order:5001"), anyString(), anyLong(), any(TimeUnit.class)); + + // 3. 验证设备状态服务更新工单信息 + verify(badgeDeviceStatusService).setCurrentOrderInfo(eq(5001L), eq(1001L), + eq(WorkOrderStatusEnum.ARRIVED.getStatus()), eq(101L), eq("F0:C8:60:1D:10:BB")); } @Test @@ -326,6 +333,7 @@ public class CleanOrderEndToEndTest { void testCP01_SignalLossAutoComplete() throws Exception { injectField(completeEventHandler, "orderLifecycleManager", orderLifecycleManager); injectField(completeEventHandler, "cleanOrderService", cleanOrderService); + injectField(completeEventHandler, "badgeDeviceStatusService", badgeDeviceStatusService); injectField(cleanOrderService, "dispatchEngine", dispatchEngine); // 准备工单:状态 ARRIVED @@ -344,7 +352,7 @@ public class CleanOrderEndToEndTest { "\"triggerSource\":\"IOT_SIGNAL_LOSS\"," + "\"triggerData\":{\"durationMs\":1800000}" + "}"; - + // 模拟 autoDispatchNext 调用成功 when(dispatchEngine.autoDispatchNext(eq(orderId), eq(2001L))) .thenReturn(DispatchResult.success("Success", 2001L)); @@ -356,8 +364,8 @@ public class CleanOrderEndToEndTest { // 1. 验证调用了 completeOrder verify(orderLifecycleManager).completeOrder(eq(orderId), eq(null), contains("信号丢失超时")); - // 2. 验证清理了 Redis 缓存 - verify(stringRedisTemplate).delete(contains("ops:clean:device:order:5001")); + // 2. 验证清理了设备工单缓存 + verify(badgeDeviceStatusService).clearCurrentOrder(5001L); // 3. 验证触发了自动调度下一单 verify(dispatchEngine).autoDispatchNext(eq(orderId), eq(2001L)); diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/queue/OrderQueueService.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/queue/OrderQueueService.java index e5dfdcf..b4d80e3 100644 --- a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/queue/OrderQueueService.java +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/queue/OrderQueueService.java @@ -8,7 +8,7 @@ import java.util.List; /** * 工单队列管理服务接口 * 提供工单队列的核心管理功能,包括入队、出队、状态变更、优先级调整等 - * + * @deprecated userId 参数实际存储的是工牌设备ID * @author lzh */ public interface OrderQueueService { @@ -18,7 +18,7 @@ public interface OrderQueueService { * 将工单加入派单队列,等待派单 * * @param opsOrderId 工单ID - * @param userId 执行人员ID(保洁员) + * @param userId 执行人员ID((保洁员/工牌设备ID)) * @param priority 优先级 * @param queueIndex 队列顺序(可选,用于同优先级排序) * @return 队列记录ID diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/handler/EventPublishHandler.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/handler/EventPublishHandler.java index 2336914..912a6d6 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/handler/EventPublishHandler.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/handler/EventPublishHandler.java @@ -33,6 +33,18 @@ public class EventPublishHandler extends TransitionHandler { log.debug("事件发布处理器: orderId={}, {} -> {}", context.getOrder().getId(), oldStatus, targetStatus); try { + // 构建事件 payload(包含 assigneeId) + java.util.Map payload = request.getPayload() != null + ? new java.util.HashMap<>(request.getPayload()) + : new java.util.HashMap<>(); + if (request.getAssigneeId() != null) { + payload.put("assigneeId", request.getAssigneeId()); + } + // 添加 urgentOrderId(P0打断场景) + if (request.getUrgentOrderId() != null) { + payload.put("urgentOrderId", request.getUrgentOrderId()); + } + // 发布状态变更事件 OrderStateChangedEvent event = OrderStateChangedEvent.builder() .orderId(context.getOrder().getId()) @@ -44,7 +56,7 @@ public class EventPublishHandler extends TransitionHandler { .operatorType(request.getOperatorType()) .eventTime(java.time.LocalDateTime.now()) .remark(request.getReason()) - .payload(request.getPayload()) + .payload(payload) .build(); orderEventPublisher.publishStateChanged(event); diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/EventDomain.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/EventDomain.java index 109bc45..3589b4a 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/EventDomain.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/EventDomain.java @@ -25,7 +25,7 @@ public enum EventDomain { TRAFFIC("traffic", "客流"), /** - * 设备域 - 设备控制、TTS��震动等 + * 设备域 - 设备控制、TTS、震动等 */ DEVICE("device", "设备"),