From 568d37a0be89a5694d9c6560933bdea65c8a4ffc Mon Sep 17 00:00:00 2001 From: lzh Date: Mon, 19 Jan 2026 13:32:23 +0800 Subject: [PATCH] feat(ops): refactor-order-operations --- .../CleanOrderAutoCreateReqDTO.java | 14 + .../CleanOrderCreateEventHandler.java | 77 +- .../consumer/DeviceEventEventHandler.java | 152 -- .../consumer/DevicePropertyEventHandler.java | 117 -- .../consumer/DeviceStatusEventHandler.java | 118 -- .../dto/DeviceEventOccurredEventDTO.java | 40 - .../dto/DevicePropertyChangedEventDTO.java | 33 - .../dto/DeviceStatusChangedEventDTO.java | 36 - .../listener/CleanOrderEventListener.java} | 1217 +++++++++-------- .../listener/CleanerStateChangeListener.java | 160 --- .../listener/OrderCreatedEventListener.java | 91 -- .../cleanorder/CleanOrderExtQueryHandler.java | 107 ++ .../service/cleanorder/CleanOrderService.java | 2 +- .../cleanorder/CleanOrderServiceImpl.java | 148 +- .../CleanerPriorityScheduleStrategy.java | 3 +- .../CleanOrderExtQueryHandlerTest.java | 203 +++ .../core/lifecycle/OrderLifecycleManager.java | 4 +- .../code/OrderCodeGenerator.java | 93 ++ .../infrastructure/id/OrderIdGenerator.java | 55 + .../infrastructure/id/OrderIdProperties.java | 28 + .../id/SnowflakeIdGenerator.java | 184 +++ .../module/ops/service/OrderDetailVO.java | 164 +++ .../ops/service/OrderExtQueryHandler.java | 46 + .../module/ops/service/OrderQueryService.java | 220 +++ .../ops/service/OrderQueryServiceImpl.java | 186 +++ .../module/ops/service/OrderSummaryVO.java | 103 ++ .../code/OrderCodeGeneratorTest.java | 182 +++ .../id/SnowflakeIdGeneratorTest.java | 178 +++ .../admin/OrderCenterController.java | 93 ++ .../src/main/resources/application.yaml | 7 + .../admin/OrderCenterControllerTest.java | 201 +++ 31 files changed, 2806 insertions(+), 1456 deletions(-) delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DeviceEventEventHandler.java delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DevicePropertyEventHandler.java delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DeviceStatusEventHandler.java delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DeviceEventOccurredEventDTO.java delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DevicePropertyChangedEventDTO.java delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DeviceStatusChangedEventDTO.java rename viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/{handler/CleanOrderEventHandler.java => integration/listener/CleanOrderEventListener.java} (60%) delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanerStateChangeListener.java delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/OrderCreatedEventListener.java create mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderExtQueryHandler.java create mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderExtQueryHandlerTest.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGenerator.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/OrderIdGenerator.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/OrderIdProperties.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/SnowflakeIdGenerator.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderDetailVO.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderExtQueryHandler.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderQueryService.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderQueryServiceImpl.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderSummaryVO.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGeneratorTest.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/infrastructure/id/SnowflakeIdGeneratorTest.java create mode 100644 viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/OrderCenterController.java create mode 100644 viewsh-module-ops/viewsh-module-ops-server/src/test/java/com/viewsh/module/ops/controller/admin/OrderCenterControllerTest.java 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 db3e3d3..49788da 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 @@ -30,4 +30,18 @@ public class CleanOrderAutoCreateReqDTO extends OpsOrderCreateReqDTO { @Min(value = 1, message = "难度等级必须大于0") private Integer difficultyLevel; + // ==================== 集成字段(IoT触发相关)==================== + + @Schema(description = "触发来源(IOT_TRAFFIC=客流阈值/IOT_BEACON=蓝牙信标/IOT_SIGNAL_LOSS=信号丢失超时)") + private String triggerSource; + + @Schema(description = "触发规则ID(关联 ops_area_device_relation.id)") + private Long triggerRuleId; + + @Schema(description = "触发设备ID(关联 iot_device.id)") + private Long triggerDeviceId; + + @Schema(description = "触发设备Key(冗余,便于查询)") + private String triggerDeviceKey; + } 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 ac88c41..f9c51f3 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 @@ -3,11 +3,10 @@ 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.dal.dataobject.dto.OpsOrderCreateReqDTO; import com.viewsh.module.ops.enums.PriorityEnum; -import com.viewsh.module.ops.enums.SourceTypeEnum; +import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO; import com.viewsh.module.ops.environment.integration.dto.CleanOrderCreateEventDTO; -import com.viewsh.module.ops.service.order.OpsOrderService; +import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; @@ -56,7 +55,7 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { private StringRedisTemplate stringRedisTemplate; @Resource - private OpsOrderService opsOrderService; + private CleanOrderService cleanOrderService; @Resource private IotDeviceControlApi iotDeviceControlApi; @@ -93,28 +92,33 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { log.info("[CleanOrderCreateEventHandler] 收到工单创建事件: eventId={}, areaId={}, triggerSource={}", event.getEventId(), event.getAreaId(), event.getTriggerSource()); - // 1. 构建创建请求 - OpsOrderCreateReqDTO createReq = new OpsOrderCreateReqDTO(); - createReq.setOrderType(event.getOrderType()); - createReq.setSourceType(SourceTypeEnum.TRAFFIC.getType()); // 系统触发 + // 1. 构建创建请求(使用 CleanOrderAutoCreateReqDTO) + CleanOrderAutoCreateReqDTO createReq = new CleanOrderAutoCreateReqDTO(); + createReq.setOrderType("CLEAN"); + createReq.setSourceType("TRAFFIC"); // 系统触发 createReq.setTitle(generateOrderTitle(event)); createReq.setDescription(generateOrderDescription(event)); createReq.setPriority(PriorityEnum.fromPriority(event.getPriority() != null ? Integer.parseInt(event.getPriority()) : 2).getPriority()); createReq.setAreaId(event.getAreaId()); + // location 字段由 areaId 自动关联,不需要在事件中传递 - // 2. 创建工单 - Long orderId = opsOrderService.createOrder(createReq); + // 扩展字段 + createReq.setExpectedDuration(calculateExpectedDuration(event)); + createReq.setCleaningType("ROUTINE"); // 可根据triggerSource动态设置 + createReq.setDifficultyLevel(3); - // 3. 更新工单的触发信息(集成字段) - opsOrderService.updateIntegrationFields( - orderId, - event.getTriggerSource(), - event.getTriggerDeviceId(), - event.getTriggerDeviceKey() - ); + // IoT集成字段 + createReq.setTriggerSource(event.getTriggerSource()); + createReq.setTriggerRuleId(extractRuleId(event)); + createReq.setTriggerDeviceId(event.getTriggerDeviceId()); + createReq.setTriggerDeviceKey(event.getTriggerDeviceKey()); - // 4. 如果是客流触发的工单,重置客流计数器基准值 + // 2. 创建工单(同时创建主表+扩展表) + Long orderId = cleanOrderService.createAutoCleanOrder(createReq); + + // 3. 如果是客流触发的工单,重置客流计数器基准值 + // TODO: 需要优化这个工单是否创建成功,才重置 if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) { resetTrafficCounter(event, orderId); } @@ -181,6 +185,8 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { return "客流阈值触发保洁"; } else if ("IOT_BEACON".equals(event.getTriggerSource())) { return "信标检测触发保洁"; + } else if ("IOT_SIGNAL_LOSS".equals(event.getTriggerSource())) { + return "离线超时触发保洁"; } else { return "IoT设备触发保洁"; } @@ -205,4 +211,39 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { return desc.toString(); } + + /** + * 计算预计作业时长(分钟) + */ + private Integer calculateExpectedDuration(CleanOrderCreateEventDTO event) { + // 根据触发来源和客流数据估算时长 + if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) { + Object actualCountObj = event.getTriggerData().get("actualCount"); + if (actualCountObj != null) { + Long actualCount = ((Number) actualCountObj).longValue(); + // 客流越大,预计耗时越长 + if (actualCount > 50) { + return 45; // 高客流 + } else if (actualCount > 20) { + return 30; // 中客流 + } else { + return 20; // 低客流 + } + } + } + return 30; // 默认30分钟 + } + + /** + * 从事件数据中提取规则ID + */ + private Long extractRuleId(CleanOrderCreateEventDTO event) { + if (event.getTriggerData() != null && event.getTriggerData().containsKey("ruleId")) { + Object ruleIdObj = event.getTriggerData().get("ruleId"); + if (ruleIdObj instanceof Number) { + return ((Number) ruleIdObj).longValue(); + } + } + return null; + } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DeviceEventEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DeviceEventEventHandler.java deleted file mode 100644 index 2163139..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DeviceEventEventHandler.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.viewsh.module.ops.environment.integration.consumer; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.viewsh.module.ops.environment.integration.dto.DeviceEventOccurredEventDTO; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.rocketmq.spring.annotation.ConsumeMode; -import org.springframework.stereotype.Component; - -/** - * 设备事件上报消费者 - *

- * 订阅 IoT 模块发布的设备事件上报(SOS、按键等) - *

- * RocketMQ 配置: - * - Topic: integration.device.event - * - Tag: productKey(可配置,如 cleaner_badge_v1) - * - * @author lzh - */ -@Slf4j -@Component -@org.apache.rocketmq.spring.annotation.RocketMQMessageListener( - topic = "integration.device.event", - consumerGroup = "ops-device-event-group", - consumeMode = ConsumeMode.CONCURRENTLY, - selectorExpression = "*" -) -public class DeviceEventEventHandler implements org.apache.rocketmq.spring.core.RocketMQListener { - - @Resource - private ObjectMapper objectMapper; - - @Resource - private IntegrationEventDeduplicationService deduplicationService; - - @Override - public void onMessage(String message) { - try { - // 1. JSON 反序列化 - DeviceEventOccurredEventDTO event = objectMapper.readValue(message, DeviceEventOccurredEventDTO.class); - - // 2. 幂等性检查 - if (!deduplicationService.tryConsume(event.getEventId())) { - log.debug("[DeviceEventEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId()); - return; - } - - // 3. 业务处理 - handleEventOccurred(event); - - } catch (Exception e) { - log.error("[DeviceEventEventHandler] 消息处理失败: message={}", message, e); - throw new RuntimeException("设备事件上报处理失败", e); - } - } - - /** - * 处理设备事件 - */ - private void handleEventOccurred(DeviceEventOccurredEventDTO event) { - Long deviceId = event.getDeviceId(); - String eventIdentifier = event.getEventIdentifier(); - - log.info("[DeviceEventEventHandler] 设备事件: deviceId={}, event={}, productKey={}", - deviceId, eventIdentifier, event.getProductKey()); - - // 根据事件类型分发处理 - switch (eventIdentifier) { - case "sos": - handleSosEvent(event); - break; - case "button_click": - handleButtonClick(event); - break; - case "fall_detected": - handleFallDetected(event); - break; - case "confirm_order": - handleConfirmOrder(event); - break; - case "complete_order": - handleCompleteOrder(event); - break; - case "pause_order": - handlePauseOrder(event); - break; - default: - log.debug("[DeviceEventEventHandler] 未知事件类型: {}", eventIdentifier); - break; - } - - log.debug("[DeviceEventEventHandler] 处理完成: deviceId={}, event={}", deviceId, eventIdentifier); - } - - /** - * 处理 SOS 告警事件 - */ - private void handleSosEvent(DeviceEventOccurredEventDTO event) { - log.warn("[DeviceEventEventHandler] SOS 告警: deviceId={}, productKey={}", - event.getDeviceId(), event.getProductKey()); - - // TODO: 触发紧急工单或告警 - // 1. 查找设备关联的保洁员 - // 2. 发送告警通知 - // 3. 创建紧急工单 - } - - /** - * 处理按键点击事件 - */ - private void handleButtonClick(DeviceEventOccurredEventDTO event) { - log.info("[DeviceEventEventHandler] 按键点击: deviceId={}", event.getDeviceId()); - } - - /** - * 处理跌倒检测事件 - */ - private void handleFallDetected(DeviceEventOccurredEventDTO event) { - log.warn("[DeviceEventEventHandler] 跌倒检测: deviceId={}", event.getDeviceId()); - - // TODO: 触发紧急告警 - } - - /** - * 处理工单确认事件 - */ - private void handleConfirmOrder(DeviceEventOccurredEventDTO event) { - log.info("[DeviceEventEventHandler] 工单确认: deviceId={}", event.getDeviceId()); - - // TODO: 更新工单状态为已确认 - } - - /** - * 处理工单完成事件 - */ - private void handleCompleteOrder(DeviceEventOccurredEventDTO event) { - log.info("[DeviceEventEventHandler] 工单完成: deviceId={}", event.getDeviceId()); - - // TODO: 更新工单状态为已完成 - } - - /** - * 处理工单暂停事件 - */ - private void handlePauseOrder(DeviceEventOccurredEventDTO event) { - log.info("[DeviceEventEventHandler] 工单暂停: deviceId={}", event.getDeviceId()); - - // TODO: 更新工单状态为已暂停 - } - -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DevicePropertyEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DevicePropertyEventHandler.java deleted file mode 100644 index 91d4715..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DevicePropertyEventHandler.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.viewsh.module.ops.environment.integration.consumer; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.viewsh.module.ops.environment.integration.dto.DevicePropertyChangedEventDTO; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.rocketmq.spring.annotation.ConsumeMode; -import org.apache.rocketmq.spring.core.RocketMQListener; -import org.springframework.stereotype.Component; - -/** - * 设备属性变更事件消费者 - *

- * 订阅 IoT 模块发布的设备属性变更事件 - *

- * RocketMQ 配置: - * - Topic: integration.device.property - * - Tag: productKey(可配置,如 cleaner_badge_v1) - * - * @author lzh - */ -@Slf4j -@Component -@org.apache.rocketmq.spring.annotation.RocketMQMessageListener( - topic = "integration.device.property", - consumerGroup = "ops-device-property-group", - consumeMode = ConsumeMode.CONCURRENTLY, - selectorExpression = "*" -) -public class DevicePropertyEventHandler implements RocketMQListener { - - @Resource - private ObjectMapper objectMapper; - - @Resource - private IntegrationEventDeduplicationService deduplicationService; - - @Override - public void onMessage(String message) { - try { - // 1. JSON 反序列化 - DevicePropertyChangedEventDTO event = objectMapper.readValue(message, DevicePropertyChangedEventDTO.class); - - // 2. 幂等性检查 - if (!deduplicationService.tryConsume(event.getEventId())) { - log.debug("[DevicePropertyEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId()); - return; - } - - // 3. 业务处理 - handlePropertyChanged(event); - - } catch (Exception e) { - log.error("[DevicePropertyEventHandler] 消息处理失败: message={}", message, e); - throw new RuntimeException("设备属性变更事件处理失败", e); - } - } - - /** - * 处理设备属性变更 - */ - private void handlePropertyChanged(DevicePropertyChangedEventDTO event) { - Long deviceId = event.getDeviceId(); - var properties = event.getProperties(); - - log.info("[DevicePropertyEventHandler] 设备属性变更: deviceId={}, properties={}, productKey={}", - deviceId, properties.keySet(), event.getProductKey()); - - // 处理特定属性变更 - if (properties.containsKey("battery")) { - handleBatteryChange(deviceId, properties.get("battery")); - } - - if (properties.containsKey("signal")) { - handleSignalChange(deviceId, properties.get("signal")); - } - - // 客流传感器数据检查 - if (properties.containsKey("people_count")) { - handlePeopleCountChange(deviceId, properties.get("people_count")); - } - - log.debug("[DevicePropertyEventHandler] 处理完成: deviceId={}", deviceId); - } - - /** - * 处理电量变化 - */ - private void handleBatteryChange(Long deviceId, Object batteryValue) { - log.debug("[DevicePropertyEventHandler] 电量变化: deviceId={}, battery={}", deviceId, batteryValue); - - // TODO: 如果电量低于阈值,提示更换 - // if (batteryValue instanceof Number && ((Number) batteryValue).intValue() < 20) { - // // 触发低电量提醒 - // } - } - - /** - * 处理信号强度变化 - */ - private void handleSignalChange(Long deviceId, Object signalValue) { - log.debug("[DevicePropertyEventHandler] 信号变化: deviceId={}, signal={}", deviceId, signalValue); - } - - /** - * 处理客流数量变化 - */ - private void handlePeopleCountChange(Long deviceId, Object peopleCount) { - log.debug("[DevicePropertyEventHandler] 客流变化: deviceId={}, peopleCount={}", deviceId, peopleCount); - - // TODO: 客流达标触发保洁 - // if (peopleCount instanceof Number && ((Number) peopleCount).intValue() > threshold) { - // // 触发保洁工单 - // } - } - -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DeviceStatusEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DeviceStatusEventHandler.java deleted file mode 100644 index 5c8ad69..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/DeviceStatusEventHandler.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.viewsh.module.ops.environment.integration.consumer; - -import cn.hutool.core.util.StrUtil; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.viewsh.module.ops.environment.integration.dto.DeviceStatusChangedEventDTO; -import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.rocketmq.spring.annotation.ConsumeMode; -import org.apache.rocketmq.spring.annotation.MessageModel; -import org.springframework.stereotype.Component; - -/** - * 设备状态变更事件消费者 - *

- * 订阅 IoT 模块发布的设备状态变更事件,同步更新保洁员在线状态 - *

- * RocketMQ 配置: - * - Topic: integration.device.status - * - Tag: productKey(可配置,如 cleaner_badge_v1) - * - 消费模式: CONCURRENTLY(并发消费) - * - * @author lzh - */ -@Slf4j -@Component -@org.apache.rocketmq.spring.annotation.RocketMQMessageListener( - topic = "integration.device.status", - consumerGroup = "ops-device-status-group", - consumeMode = ConsumeMode.CONCURRENTLY, - // TAG 过滤将在配置文件中设置,或使用 selectorExpression - selectorExpression = "*" -) -public class DeviceStatusEventHandler implements org.apache.rocketmq.spring.core.RocketMQListener { - - @Resource - private CleanerStatusService cleanerStatusService; - - @Resource - private ObjectMapper objectMapper; - - @Resource - private IntegrationEventDeduplicationService deduplicationService; - - @Override - public void onMessage(String message) { - try { - // 1. JSON 反序列化 - DeviceStatusChangedEventDTO event = objectMapper.readValue(message, DeviceStatusChangedEventDTO.class); - - // 2. 幂等性检查 - if (!deduplicationService.tryConsume(event.getEventId())) { - log.debug("[DeviceStatusEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId()); - return; - } - - // 3. 业务处理 - handleStatusChanged(event); - - } catch (Exception e) { - log.error("[DeviceStatusEventHandler] 消息处理失败: message={}", message, e); - // 抛出异常让 RocketMQ 重试 - throw new RuntimeException("设备状态变更事件处理失败", e); - } - } - - /** - * 处理设备状态变更 - */ - private void handleStatusChanged(DeviceStatusChangedEventDTO event) { - Long deviceId = event.getDeviceId(); - Integer newStatus = event.getNewStatus(); - - log.info("[DeviceStatusEventHandler] 设备状态变更: deviceId={}, newStatus={}, productKey={}", - deviceId, newStatus, event.getProductKey()); - - // 只处理在线/离线状态变更 - // 1 = 在线, 0 = 离线 - if (newStatus == 1) { - // 设备上线 -> 保洁员上线 - handleDeviceOnline(event); - } else if (newStatus == 0) { - // 设备离线 -> 保洁员离线 - handleDeviceOffline(event); - } - - log.debug("[DeviceStatusEventHandler] 处理完成: deviceId={}, status={}", deviceId, newStatus); - } - - /** - * 处理设备上线 - */ - private void handleDeviceOnline(DeviceStatusChangedEventDTO event) { - // TODO: 根据设备ID查找对应的保洁员 - // Long cleanerId = getCleanerIdByDeviceId(event.getDeviceId()); - // if (cleanerId != null) { - // cleanerStatusService.updateOnlineStatus(cleanerId, true); - // } - - log.debug("[DeviceStatusEventHandler] 设备上线: deviceId={}, reason={}", - event.getDeviceId(), event.getReason()); - } - - /** - * 处理设备离线 - */ - private void handleDeviceOffline(DeviceStatusChangedEventDTO event) { - // TODO: 根据设备ID查找对应的保洁员 - // Long cleanerId = getCleanerIdByDeviceId(event.getDeviceId()); - // if (cleanerId != null) { - // cleanerStatusService.updateOnlineStatus(cleanerId, false); - // } - - log.debug("[DeviceStatusEventHandler] 设备离线: deviceId={}, reason={}", - event.getDeviceId(), event.getReason()); - } - -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DeviceEventOccurredEventDTO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DeviceEventOccurredEventDTO.java deleted file mode 100644 index ac5b1e2..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DeviceEventOccurredEventDTO.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.viewsh.module.ops.environment.integration.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.util.Map; - -/** - * 设备事件上报 DTO - *

- * 用于反序列化从 RocketMQ 接收的设备事件上报消息 - * - * @author lzh - */ -@Data -@EqualsAndHashCode(callSuper = true) -public class DeviceEventOccurredEventDTO extends BaseDeviceEventDTO { - - /** - * 事件标识符 - * 例如: button_click(按键点击), sos(紧急求救), fall_detected(跌倒检测)等 - */ - @JsonProperty("eventIdentifier") - private String eventIdentifier; - - /** - * 事件类型 - * 例如: ALARM(告警事件), CONTROL(控��事件), INFO(信息事件) - */ - @JsonProperty("eventType") - private String eventType; - - /** - * 事件参数 - */ - @JsonProperty("eventParams") - private Map eventParams; - -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DevicePropertyChangedEventDTO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DevicePropertyChangedEventDTO.java deleted file mode 100644 index b95176c..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DevicePropertyChangedEventDTO.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.viewsh.module.ops.environment.integration.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.util.Map; -import java.util.Set; - -/** - * 设备属性变更事件 DTO - *

- * 用于反序列化从 RocketMQ 接收的设备属性变更消息 - * - * @author lzh - */ -@Data -@EqualsAndHashCode(callSuper = true) -public class DevicePropertyChangedEventDTO extends BaseDeviceEventDTO { - - /** - * 变更的属性数据 - */ - @JsonProperty("properties") - private Map properties; - - /** - * 变更的属性标识符集合 - */ - @JsonProperty("changedIdentifiers") - private Set changedIdentifiers; - -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DeviceStatusChangedEventDTO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DeviceStatusChangedEventDTO.java deleted file mode 100644 index 84625a6..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/DeviceStatusChangedEventDTO.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.viewsh.module.ops.environment.integration.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 设备状态变更事件 DTO - *

- * 用于反序列化从 RocketMQ 接收的设备状态变更消息 - * - * @author lzh - */ -@Data -@EqualsAndHashCode(callSuper = true) -public class DeviceStatusChangedEventDTO extends BaseDeviceEventDTO { - - /** - * 旧状态 - */ - @JsonProperty("oldStatus") - private Integer oldStatus; - - /** - * 新状态 - */ - @JsonProperty("newStatus") - private Integer newStatus; - - /** - * 变更原因 - */ - @JsonProperty("reason") - private String reason; - -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/handler/CleanOrderEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java similarity index 60% rename from viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/handler/CleanOrderEventHandler.java rename to viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java index f3ab355..c891f5c 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/handler/CleanOrderEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java @@ -1,603 +1,614 @@ -package com.viewsh.module.ops.environment.handler; - -import cn.hutool.core.map.MapUtil; -import com.viewsh.module.ops.core.event.OrderCompletedEvent; -import com.viewsh.module.ops.core.event.OrderCreatedEvent; -import com.viewsh.module.ops.core.event.OrderStateChangedEvent; -import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO; -import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; -import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper; -import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; -import com.viewsh.module.ops.enums.WorkOrderStatusEnum; -import com.viewsh.module.ops.environment.constants.CleanNotificationConstants; -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.VoiceBroadcastDeduplicationService; -import com.viewsh.module.iot.api.device.IotDeviceControlApi; -import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO; -import com.viewsh.module.system.api.notify.NotifyMessageSendApi; -import com.viewsh.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; -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 java.time.LocalDateTime; -import java.util.Map; - -/** - * 保洁工单事件处理器 - *

- * 职责: - * 1. 订阅工单状态变更事件,处理保洁业务特定逻辑 - * 2. 记录保洁扩展信息(到岗时间、完成时间、暂停时间等) - * 3. 更新保洁员状态 - * 4. 发送通知(语音播报、站内信、IoT震动) - *

- * 设计说明: - * - 使用 @EventListener 订阅领域事件 - * - 业务逻辑同步执行 - * - 通知逻辑异步执行(@Async) - * - 消息内容使用 {@link CleanNotificationConstants} 统一管理 - *

- * - 未来迁移 RocketMQ:只需修改 @EventListener 为 @RocketMQMessageListener - * - * @author lzh - */ -@Slf4j -@Component -public class CleanOrderEventHandler { - - @Resource - private CleanerStatusService cleanerStatusService; - - @Resource - private OpsOrderCleanExtMapper cleanExtMapper; - - @Resource - private CleanOrderService cleanOrderService; - - @Resource - private OpsOrderMapper opsOrderMapper; - - @Resource - private VoiceBroadcastDeduplicationService voiceBroadcastDeduplicationService; - - /** - * 站内信发送 API(Feign 客户端,微服务架构下调用 system 服务) - */ - @Resource - private NotifyMessageSendApi notifyMessageSendApi; - - /** - * IoT 设备控制 API(Feign 客户端,微服务架构下调用 IoT 服务) - */ - @Resource - private IotDeviceControlApi iotDeviceControlApi; - - /** - * 订阅状态变更事件 - *

- * 只处理保洁类型的工单(orderType = "CLEAN") - */ - @EventListener - public void onStateChanged(OrderStateChangedEvent event) { - // 只处理保洁类型的工单 - if (!"CLEAN".equals(event.getOrderType())) { - return; - } - - log.info("保洁工单状态变更: orderId={}, {} -> {}, operatorId={}", - event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId()); - - switch (event.getNewStatus()) { - case DISPATCHED: - handleDispatched(event); - break; - case CONFIRMED: - handleConfirmed(event); - break; - case ARRIVED: - handleArrived(event); - break; - case PAUSED: - handlePaused(event); - break; - case COMPLETED: - handleCompleted(event); - break; - case CANCELLED: - handleCancelled(event); - break; - default: - break; - } - } - - /** - * 订阅工单创建事件 - */ - @EventListener - public void onOrderCreated(OrderCreatedEvent event) { - if (!"CLEAN".equals(event.getOrderType())) { - return; - } - - log.info("保洁工单已创建: orderId={}, orderCode={}, priority={}", - event.getOrderId(), event.getOrderCode(), event.getPriority()); - - // 可以在这里触发自动派单逻辑 - // 但通常派单逻辑在工单创建时直接调用 - } - - /** - * 订阅工单完成事件 - */ - @EventListener - public void onOrderCompleted(OrderCompletedEvent event) { - if (!"CLEAN".equals(event.getOrderType())) { - return; - } - - log.info("保洁工单已完成: orderId={}, assigneeId={}, workDuration={}秒", - event.getOrderId(), event.getAssigneeId(), event.getWorkDuration()); - - // 自动推送下一个任务 - if (event.getAssigneeId() != null) { - cleanOrderService.autoDispatchNextOrder(event.getOrderId(), event.getAssigneeId()); - } - - // 发送完成通知(异步) - sendOrderCompletedNotification(event.getOrderId()); - } - - // ==================== 状态处理方法 ==================== - - /** - * 处理已推送状态(工单已推送到工牌) - */ - private void handleDispatched(OrderStateChangedEvent event) { - Long orderId = event.getOrderId(); - Long cleanerId = event.getOperatorId(); - - // 发送新工单通知(语音+震动+站内信) - sendNewOrderNotification(cleanerId, orderId); - } - - /** - * 处理已确认状态(保洁员按下确认按钮) - */ - private void handleConfirmed(OrderStateChangedEvent event) { - Long cleanerId = event.getOperatorId(); - - // 更新保洁员状态为忙碌 - cleanerStatusService.updateStatus(cleanerId, - com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY, - "确认工单"); - - log.info("保洁员已确认工单,状态更新为BUSY: cleanerId={}", cleanerId); - } - - /** - * 处理到岗事件 - */ - private void handleArrived(OrderStateChangedEvent event) { - Long orderId = event.getOrderId(); - - // 1. 记录到岗时间到保洁扩展表 - OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); - updateObj.setOpsOrderId(orderId); - updateObj.setArrivedTime(LocalDateTime.now()); - cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); - - log.info("保洁员已到岗: orderId={}", orderId); - } - - /** - * 处理暂停事件 - */ - private void handlePaused(OrderStateChangedEvent event) { - Long orderId = event.getOrderId(); - Long operatorId = event.getOperatorId(); - - // 检查是否是被P0任务打断 - String interruptReason = event.getPayloadString("interruptReason"); - if ("P0_TASK_INTERRUPT".equals(interruptReason)) { - Long urgentOrderId = event.getPayloadLong("urgentOrderId"); - - log.warn("保洁任务被P0任务打断: orderId={}, urgentOrderId={}", orderId, urgentOrderId); - - // 释放保洁员资源 - cleanerStatusService.clearCurrentWorkOrder(operatorId); - - // 记录暂停开始时间 - recordPauseStartTime(orderId); - } else { - // 普通暂停 - log.info("保洁任务已暂停: orderId={}, operatorId={}", orderId, operatorId); - recordPauseStartTime(orderId); - } - } - - /** - * 处理完成事件 - */ - private void handleCompleted(OrderStateChangedEvent event) { - Long orderId = event.getOrderId(); - - // 计算作业时长 - Integer actualDuration = cleanOrderService.calculateActualDuration(orderId); - log.info("保洁作业完成: orderId={}, actualDuration={}秒", orderId, actualDuration); - - // 记录完成时间和时长到保洁扩展表 - OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); - updateObj.setOpsOrderId(orderId); - updateObj.setCompletedTime(LocalDateTime.now()); - cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); - - // 保洁员状态恢复(由 autoDispatchNextOrder 处理) - } - - /** - * 处理取消事件 - */ - private void handleCancelled(OrderStateChangedEvent event) { - Long orderId = event.getOrderId(); - Long operatorId = event.getOperatorId(); - - log.info("保洁工单已取消: orderId={}, operatorId={}", orderId, operatorId); - - // 清理保洁员当前工单 - if (operatorId != null) { - cleanerStatusService.clearCurrentWorkOrder(operatorId); - } - } - - // ==================== 通知方法(异步)==================== - - /** - * 发送新工单通知(语音播报 + 震动提醒 + 站内信) - * - * @param cleanerId 保洁员ID - * @param orderId 工单ID - */ - @Async("ops-task-executor") - public void sendNewOrderNotification(Long cleanerId, Long orderId) { - try { - OpsOrderDO order = opsOrderMapper.selectById(orderId); - if (order == null) { - log.warn("[新工单通知] 工单不存在: orderId={}", orderId); - return; - } - - log.info("[新工单通知] cleanerId={}, orderId={}", cleanerId, orderId); - - // 1. 语音播报:使用常量 - playVoice(cleanerId, CleanNotificationConstants.VoiceMessage.NEW_ORDER); - - // 2. 震动提醒:使用常量 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, - CleanNotificationConstants.TemplateCode.NEW_ORDER, - CleanNotificationConstants.NotifyParamsBuilder.newOrderParams( - order.getOrderCode(), - order.getTitle(), - getAreaName(order.getAreaId()) - )); - - } catch (Exception e) { - log.error("[新工单通知] 发送失败: cleanerId={}, orderId={}", cleanerId, orderId, e); - } - } - - /** - * 发送待办增加通知(支持去重合并) - * - * @param cleanerId 保洁员ID - * @param queueCount 当前待办数量 - */ - @Async("ops-task-executor") - public void sendQueuedOrderNotification(Long cleanerId, int queueCount) { - try { - log.info("[待办增加通知] cleanerId={}, queueCount={}", cleanerId, queueCount); - - // 1. 使用去重服务合并播报 - voiceBroadcastDeduplicationService.recordAndBroadcast(cleanerId, 1, false); - - // 2. 震动提醒(轻量震动):使用常量 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.LIGHT); - - // 3. 发送站内信(可选,避免频繁打扰) - // 如果待办数量较多时才发送站内信 - if (queueCount >= 3) { - sendNotifyMessageToMember(cleanerId, - CleanNotificationConstants.TemplateCode.QUEUED_ORDER, - CleanNotificationConstants.NotifyParamsBuilder.queuedOrderParams(queueCount, 1)); - } - - } catch (Exception e) { - log.error("[待办增加通知] 发送失败: cleanerId={}", cleanerId, e); - } - } - - /** - * 发送下一个任务通知 - * - * @param cleanerId 保洁员ID - * @param queueCount 待办数量 - * @param orderTitle 任务标题 - */ - @Async("ops-task-executor") - public void sendNextTaskNotification(Long cleanerId, int queueCount, String orderTitle) { - try { - log.info("[下一任务通知] cleanerId={}, queueCount={}, title={}", cleanerId, queueCount, orderTitle); - - // 1. 语音播报:使用常量类格式化 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatNextTask(queueCount, orderTitle); - playVoice(cleanerId, voiceMessage); - - // 2. 震动提醒:使用常量 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, - CleanNotificationConstants.TemplateCode.NEXT_TASK, - CleanNotificationConstants.NotifyParamsBuilder.nextTaskParams(queueCount, orderTitle)); - - } catch (Exception e) { - log.error("[下一任务通知] 发送失败: cleanerId={}", cleanerId, e); - } - } - - /** - * 发送P0紧急任务插队通知 - * - * @param cleanerId 保洁员ID - * @param orderCode 工单编号 - */ - @Async("ops-task-executor") - public void sendPriorityUpgradeNotification(Long cleanerId, String orderCode) { - try { - log.warn("[P0紧急通知] cleanerId={}, orderCode={}", cleanerId, orderCode); - - // 1. 语音播报:使用常量类格式化 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatPriorityUpgrade(orderCode); - playVoice(cleanerId, voiceMessage); - - // 2. 强烈震动提醒:使用常量 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.STRONG); - - // 3. 发送站内信(高优先级) - sendNotifyMessageToMember(cleanerId, - CleanNotificationConstants.TemplateCode.PRIORITY_UPGRADE, - CleanNotificationConstants.NotifyParamsBuilder.priorityUpgradeParams(orderCode, "P0紧急任务")); - - } catch (Exception e) { - log.error("[P0紧急通知] 发送失败: cleanerId={}", cleanerId, e); - } - } - - /** - * 发送任务恢复通知 - * - * @param cleanerId 保洁员ID - * @param areaName 区域名称 - */ - @Async("ops-task-executor") - public void sendTaskResumedNotification(Long cleanerId, String areaName) { - try { - log.info("[任务恢复通知] cleanerId={}, areaName={}", cleanerId, areaName); - - // 1. 语音播报:使用常量类格式化 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatTaskResumed(areaName); - playVoice(cleanerId, voiceMessage); - - // 2. 震动提醒:使用常量 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, - CleanNotificationConstants.TemplateCode.TASK_RESUMED, - CleanNotificationConstants.NotifyParamsBuilder.taskResumedParams(areaName)); - - } catch (Exception e) { - log.error("[任务恢复通知] 发送失败: cleanerId={}", cleanerId, e); - } - } - - /** - * 发送工单完成通知(通知巡检员验收) - * - * @param orderId 工单ID - */ - @Async("ops-task-executor") - public void sendOrderCompletedNotification(Long orderId) { - try { - OpsOrderDO order = opsOrderMapper.selectById(orderId); - if (order == null) { - return; - } - - log.info("[工单完成通知] orderId={}, orderCode={}, areaId={}", - orderId, order.getOrderCode(), order.getAreaId()); - - // TODO: 查询该区域的巡检员列表 - // List inspectorIds = inspectorService.listInspectorsByArea(order.getAreaId()); - // for (Long inspectorId : inspectorIds) { - // // 发送站内信给巡检员 - // sendNotifyMessageToAdmin(inspectorId, - // CleanNotificationConstants.TemplateCode.ORDER_COMPLETED, - // CleanNotificationConstants.NotifyParamsBuilder.orderCompletedParams( - // order.getOrderCode(), getAreaName(order.getAreaId()), order.getTitle())); - // } - - } catch (Exception e) { - log.error("[工单完成通知] 发送失败: orderId={}", orderId, e); - } - } - - // ==================== IoT 设备操作方法 ==================== - - /** - * 语音播报 - *

- * 通过 RPC 调用 IoT 服务的设备控制接口 - * - * @param cleanerId 保洁员ID - * @param message 播报内容 - */ - private void playVoice(Long cleanerId, String message) { - try { - // 获取保洁员关联的工牌设备ID - Long deviceId = getBadgeDeviceId(cleanerId); - if (deviceId == null) { - log.warn("[语音播报] 保洁员无关联工牌设备: cleanerId={}", cleanerId); - return; - } - - // 构建服务调用请求 - IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO(); - reqDTO.setDeviceId(deviceId); - reqDTO.setIdentifier("playVoice"); - reqDTO.setParams(MapUtil.builder() - .put("text", message) - .put("volume", 80) - .build()); - - // 调用 IoT 服务 - iotDeviceControlApi.invokeService(reqDTO); - log.info("[语音播报] 调用成功: cleanerId={}, deviceId={}, message={}", cleanerId, deviceId, message); - - } catch (Exception e) { - log.error("[语音播报] 调用失败: cleanerId={}, message={}", cleanerId, message, e); - } - } - - /** - * 震动提醒 - *

- * 通过 RPC 调用 IoT 服务的设备控制接口 - * - * @param cleanerId 保洁员ID - * @param durationMs 震动时长(毫秒) - */ - private void vibrate(Long cleanerId, int durationMs) { - try { - // 获取保洁员关联的工牌设备ID - Long deviceId = getBadgeDeviceId(cleanerId); - if (deviceId == null) { - log.warn("[震动提醒] 保洁员无关联工牌设备: cleanerId={}", cleanerId); - return; - } - - // 构建服务调用请求 - IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO(); - reqDTO.setDeviceId(deviceId); - reqDTO.setIdentifier("vibrate"); - reqDTO.setParams(MapUtil.builder() - .put("duration", durationMs) - .put("intensity", 50) - .build()); - - // 调用 IoT 服务 - iotDeviceControlApi.invokeService(reqDTO); - log.info("[震动提醒] 调用成功: cleanerId={}, deviceId={}, durationMs={}", cleanerId, deviceId, durationMs); - - } catch (Exception e) { - log.error("[震动提醒] 调用失败: cleanerId={}, durationMs={}", cleanerId, durationMs, e); - } - } - - /** - * 获取保洁员关联的工牌设备ID - *

- * TODO: 需要根据实际业务逻辑实现 - * - 方案1:在保洁员表维护关联的设备ID - * - 方案2:通过用户ID查询设备表(设备表有userId字段) - * - 方案3:通过中间表维护保洁员与设备的关系 - * - * @param cleanerId 保洁员ID - * @return 工牌设备ID,无关联返回null - */ - private Long getBadgeDeviceId(Long cleanerId) { - // TODO: 实现获取保洁员关联的工牌设备ID - // 当前返回null,实际使用时需要实现具体逻辑 - // 示例: - // OpsCleanerDeviceDO device = cleanerDeviceMapper.selectByCleanerId(cleanerId); - // return device != null ? device.getDeviceId() : null; - - log.debug("[getBadgeDeviceId] 查询保洁员设备: cleanerId={}", cleanerId); - return null; - } - - // ==================== 站内信发送方法 ==================== - - /** - * 发送站内信给 Member 用户(保洁员) - * - * @param userId 用户ID - * @param templateCode 模板代码 - * @param templateParams 模板参数 - */ - private void sendNotifyMessageToMember(Long userId, String templateCode, - java.util.Map templateParams) { - try { - NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO(); - reqDTO.setUserId(userId); - reqDTO.setTemplateCode(templateCode); - reqDTO.setTemplateParams(templateParams); - - notifyMessageSendApi.sendSingleMessageToMember(reqDTO); - log.info("[站内信发送成功] userId={}, templateCode={}", userId, templateCode); - - } catch (Exception e) { - log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e); - } - } - - /** - * 发送站内信给 Admin 用户(巡检员) - * - * @param userId 用户ID - * @param templateCode 模板代码 - * @param templateParams 模板参数 - */ - 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.info("[站内信发送成功] userId={}, templateCode={}", userId, templateCode); - - } catch (Exception e) { - log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e); - } - } - - // ==================== 辅助方法 ==================== - - /** - * 获取区域名称 - */ - private String getAreaName(Long areaId) { - // TODO: 从区域服务获取区域名称 - return "某区域"; - } - - /** - * 记录暂停开始时间 - */ - private void recordPauseStartTime(Long orderId) { - OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); - updateObj.setOpsOrderId(orderId); - updateObj.setPauseStartTime(LocalDateTime.now()); - cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); - } -} +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.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.OrderCompletedEvent; +import com.viewsh.module.ops.core.event.OrderCreatedEvent; +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.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.VoiceBroadcastDeduplicationService; +import com.viewsh.module.system.api.notify.NotifyMessageSendApi; +import com.viewsh.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.time.LocalDateTime; + +/** + * 保洁工单事件监听器(统一入口) + *

+ * 职责: + * 1. 监听工单创建事件,触发自动调度 + * 2. 监听工单状态变更事件,处理: + * - 扩展表时间记录(到岗、完成、暂停) + * - 保洁员状态同步 + * - 通知发送(语音、震动、站内信) + * 3. 监听工单完成事件,触发自动派单下一个任务 + *

+ * 设计说明: + * - 整合了 OrderCreatedEventListener、CleanerStateChangeListener、CleanOrderEventHandler 的功能 + * - 使用异步处理,避免阻塞主流程 + * - 使用 @TransactionalEventListener(AFTER_COMMIT) 确保事务提交后再处理 + * + * @author lzh + */ +@Slf4j +@Component +public class CleanOrderEventListener { + + @Resource + private DispatchEngine dispatchEngine; + + @Resource + private OpsOrderMapper opsOrderMapper; + + @Resource + private OpsOrderCleanExtMapper cleanExtMapper; + + @Resource + private CleanerStatusService cleanerStatusService; + + @Resource + private CleanOrderService cleanOrderService; + + @Resource + private VoiceBroadcastDeduplicationService voiceBroadcastDeduplicationService; + + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + + @Resource + private IotDeviceControlApi iotDeviceControlApi; + + // ==================== 工单创建事件 ==================== + + /** + * 监听工单创建事件,触发自动调度 + *

+ * 使用 @TransactionalEventListener 确保在事务提交后才执行调度 + * 避免调度失败导致工单创建回滚 + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onOrderCreated(OrderCreatedEvent event) { + if (!"CLEAN".equals(event.getOrderType())) { + return; + } + + log.info("[CleanOrderEventListener] 工单创建事件: orderId={}, priority={}, areaId={}", + event.getOrderId(), event.getPriority(), event.getAreaId()); + + // 异步触发调度 + asyncDispatchAfterCreated(event); + } + + /** + * 异步执行调度 + */ + @Async("ops-task-executor") + public void asyncDispatchAfterCreated(OrderCreatedEvent event) { + try { + OpsOrderDO order = opsOrderMapper.selectById(event.getOrderId()); + if (order == null) { + log.warn("[CleanOrderEventListener] 工单不存在,无法调度: orderId={}", event.getOrderId()); + return; + } + + // 构建调度上下文 + OrderDispatchContext context = OrderDispatchContext.builder() + .orderId(order.getId()) + .orderCode(order.getOrderCode()) + .orderTitle(order.getTitle()) + .businessType(order.getOrderType()) + .areaId(order.getAreaId() != null ? order.getAreaId() : event.getAreaId()) + .priority(PriorityEnum.fromPriority(event.getPriority())) + .build(); + + // 执行调度(包含 P0 打断逻辑) + DispatchResult result = dispatchEngine.dispatch(context); + + if (result.isSuccess()) { + log.info("[CleanOrderEventListener] 自动调度成功: orderId={}, assigneeId={}", + event.getOrderId(), result.getAssigneeId()); + } else { + log.warn("[CleanOrderEventListener] 自动调度失败: orderId={}, reason={}", + event.getOrderId(), result.getMessage()); + } + + } catch (Exception e) { + log.error("[CleanOrderEventListener] 自动调度异常: orderId={}", event.getOrderId(), e); + } + } + + // ==================== 状态变更事件 ==================== + + /** + * 监听状态变更事件 + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onOrderStateChanged(OrderStateChangedEvent event) { + if (!"CLEAN".equals(event.getOrderType())) { + return; + } + + log.info("[CleanOrderEventListener] 状态变更: orderId={}, {} -> {}, operatorId={}", + event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId()); + + switch (event.getNewStatus()) { + case DISPATCHED: + handleDispatched(event); + break; + case CONFIRMED: + handleConfirmed(event); + break; + case ARRIVED: + handleArrived(event); + break; + case PAUSED: + handlePaused(event); + break; + case COMPLETED: + handleCompleted(event); + break; + case CANCELLED: + handleCancelled(event); + break; + default: + break; + } + } + + /** + * 处理已推送状态 + */ + @Async("ops-task-executor") + public void handleDispatched(OrderStateChangedEvent event) { + // 发送新工单通知(语音+震动+站内信) + sendNewOrderNotification(event.getOperatorId(), event.getOrderId()); + } + + /** + * 处理已确认状态 + */ + @Async("ops-task-executor") + public void handleConfirmed(OrderStateChangedEvent event) { + Long cleanerId = event.getOperatorId(); + if (cleanerId == null) { + return; + } + + // 更新保洁员状态为 BUSY + cleanerStatusService.updateStatus(cleanerId, + com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY, + "确认工单"); + + // 设置当前工单 + cleanerStatusService.setCurrentWorkOrder(cleanerId, event.getOrderId(), + opsOrderMapper.selectById(event.getOrderId()).getOrderCode()); + + log.info("[CleanOrderEventListener] 保洁员确认工单,状态更新为BUSY: cleanerId={}", cleanerId); + } + + /** + * 处理到岗状态 + */ + @Async("ops-task-executor") + public void handleArrived(OrderStateChangedEvent event) { + Long orderId = event.getOrderId(); + + // 1. 记录到岗时间到扩展表 + OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); + updateObj.setOpsOrderId(orderId); + updateObj.setArrivedTime(LocalDateTime.now()); + cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); + + log.info("[CleanOrderEventListener] 保洁员已到岗: orderId={}", orderId); + } + + /** + * 处理暂停状态 + */ + @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); + } + + /** + * 处理完成状态 + */ + @Async("ops-task-executor") + public void handleCompleted(OrderStateChangedEvent event) { + Long orderId = event.getOrderId(); + + // 1. 计算作业时长 + Integer actualDuration = cleanOrderService.calculateActualDuration(orderId); + + // 2. 记录完成时间到扩展表 + OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); + updateObj.setOpsOrderId(orderId); + updateObj.setCompletedTime(LocalDateTime.now()); + cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); + + log.info("[CleanOrderEventListener] 保洁作业完成: orderId={}, actualDuration={}秒", orderId, actualDuration); + } + + /** + * 处理取消状态 + */ + @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()); + } + + // ==================== 工单完成事件 ==================== + + /** + * 监听工单完成事件 + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onOrderCompleted(OrderCompletedEvent event) { + if (!"CLEAN".equals(event.getOrderType())) { + return; + } + + log.info("[CleanOrderEventListener] 工单完成: orderId={}, assigneeId={}", + event.getOrderId(), event.getAssigneeId()); + + Long assigneeId = event.getAssigneeId(); + if (assigneeId != null) { + // 1. 清理当前工单 + cleanerStatusService.clearCurrentWorkOrder(assigneeId); + + // 2. 自动推送下一个任务(异步) + asyncDispatchNext(assigneeId, event.getOrderId()); + } + + // 3. 发送完成通知(异步) + asyncSendOrderCompletedNotification(event.getOrderId()); + } + + /** + * 异步推送下一个任务 + */ + @Async("ops-task-executor") + public void asyncDispatchNext(Long cleanerId, Long completedOrderId) { + cleanOrderService.autoDispatchNextOrder(completedOrderId, cleanerId); + } + + /** + * 异步发送完成通知 + */ + @Async("ops-task-executor") + public void asyncSendOrderCompletedNotification(Long orderId) { + sendOrderCompletedNotification(orderId); + } + + // ==================== 通知方法 ==================== + + /** + * 发送新工单通知(语音播报 + 震动提醒 + 站内信) + */ + @Async("ops-task-executor") + public void sendNewOrderNotification(Long cleanerId, Long orderId) { + try { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.warn("[新工单通知] 工单不存在: orderId={}", orderId); + return; + } + + log.info("[新工单通知] cleanerId={}, orderId={}", cleanerId, orderId); + + // 1. 语音播报 + playVoice(cleanerId, CleanNotificationConstants.VoiceMessage.NEW_ORDER); + + // 2. 震动提醒 + vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); + + // 3. 发送站内信 + sendNotifyMessageToMember(cleanerId, + CleanNotificationConstants.TemplateCode.NEW_ORDER, + CleanNotificationConstants.NotifyParamsBuilder.newOrderParams( + order.getOrderCode(), + order.getTitle(), + getAreaName(order.getAreaId()) + )); + + } catch (Exception e) { + log.error("[新工单通知] 发送失败: cleanerId={}, orderId={}", cleanerId, orderId, e); + } + } + + /** + * 发送待办增加通知 + */ + @Async("ops-task-executor") + public void sendQueuedOrderNotification(Long cleanerId, int queueCount) { + try { + log.info("[待办增加通知] cleanerId={}, queueCount={}", cleanerId, queueCount); + + // 1. 使用去重服务合并播报 + voiceBroadcastDeduplicationService.recordAndBroadcast(cleanerId, 1, false); + + // 2. 震动提醒 + vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.LIGHT); + + // 3. 发送站内信(待办数量较多时) + if (queueCount >= 3) { + sendNotifyMessageToMember(cleanerId, + CleanNotificationConstants.TemplateCode.QUEUED_ORDER, + CleanNotificationConstants.NotifyParamsBuilder.queuedOrderParams(queueCount, 1)); + } + + } catch (Exception e) { + log.error("[待办增加通知] 发送失败: cleanerId={}", cleanerId, e); + } + } + + /** + * 发送下一个任务通知 + */ + @Async("ops-task-executor") + public void sendNextTaskNotification(Long cleanerId, int queueCount, String orderTitle) { + try { + log.info("[下一任务通知] cleanerId={}, queueCount={}, title={}", cleanerId, queueCount, orderTitle); + + // 1. 语音播报 + String voiceMessage = CleanNotificationConstants.VoiceHelper.formatNextTask(queueCount, orderTitle); + playVoice(cleanerId, voiceMessage); + + // 2. 震动提醒 + vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); + + // 3. 发送站内信 + sendNotifyMessageToMember(cleanerId, + CleanNotificationConstants.TemplateCode.NEXT_TASK, + CleanNotificationConstants.NotifyParamsBuilder.nextTaskParams(queueCount, orderTitle)); + + } catch (Exception e) { + log.error("[下一任务通知] 发送失败: cleanerId={}", cleanerId, e); + } + } + + /** + * 发送P0紧急任务插队通知 + */ + @Async("ops-task-executor") + public void sendPriorityUpgradeNotification(Long cleanerId, String orderCode) { + try { + log.warn("[P0紧急通知] cleanerId={}, orderCode={}", cleanerId, orderCode); + + // 1. 语音播报 + String voiceMessage = CleanNotificationConstants.VoiceHelper.formatPriorityUpgrade(orderCode); + playVoice(cleanerId, voiceMessage); + + // 2. 强烈震动提醒 + vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.STRONG); + + // 3. 发送站内信 + sendNotifyMessageToMember(cleanerId, + CleanNotificationConstants.TemplateCode.PRIORITY_UPGRADE, + CleanNotificationConstants.NotifyParamsBuilder.priorityUpgradeParams(orderCode, "P0紧急任务")); + + } catch (Exception e) { + log.error("[P0紧急通知] 发送失败: cleanerId={}", cleanerId, e); + } + } + + /** + * 发送任务恢复通知 + */ + @Async("ops-task-executor") + public void sendTaskResumedNotification(Long cleanerId, String areaName) { + try { + log.info("[任务恢复通知] cleanerId={}, areaName={}", cleanerId, areaName); + + // 1. 语音播报 + String voiceMessage = CleanNotificationConstants.VoiceHelper.formatTaskResumed(areaName); + playVoice(cleanerId, voiceMessage); + + // 2. 震动提醒 + vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); + + // 3. 发送站内信 + sendNotifyMessageToMember(cleanerId, + CleanNotificationConstants.TemplateCode.TASK_RESUMED, + CleanNotificationConstants.NotifyParamsBuilder.taskResumedParams(areaName)); + + } catch (Exception e) { + log.error("[任务恢复通知] 发送失败: cleanerId={}", cleanerId, e); + } + } + + /** + * 发送工单完成通知 + */ + @Async("ops-task-executor") + public void sendOrderCompletedNotification(Long orderId) { + try { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + return; + } + + log.info("[工单完成通知] orderId={}, orderCode={}, areaId={}", + orderId, order.getOrderCode(), order.getAreaId()); + + // TODO: 查询该区域的巡检员列表并发送通知 + + } catch (Exception e) { + log.error("[工单完成通知] 发送失败: orderId={}", orderId, e); + } + } + + /** + * 播放语音(供外部调用) + */ + @Async("ops-task-executor") + public void playVoiceForNewOrder(Long cleanerId) { + playVoice(cleanerId, CleanNotificationConstants.VoiceMessage.NEW_ORDER); + } + + // ==================== IoT 设备操作方法 ==================== + + /** + * 语音播报 + */ + private void playVoice(Long cleanerId, String message) { + try { + Long deviceId = getBadgeDeviceId(cleanerId); + if (deviceId == null) { + log.warn("[语音播报] 保洁员无关联工牌设备: cleanerId={}", cleanerId); + return; + } + + IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO(); + reqDTO.setDeviceId(deviceId); + reqDTO.setIdentifier("playVoice"); + reqDTO.setParams(MapUtil.builder() + .put("text", message) + .put("volume", 80) + .build()); + + iotDeviceControlApi.invokeService(reqDTO); + log.debug("[语音播报] 调用成功: cleanerId={}, deviceId={}, message={}", cleanerId, deviceId, message); + + } catch (Exception e) { + log.error("[语音播报] 调用失败: cleanerId={}, message={}", cleanerId, 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) { + try { + NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO(); + reqDTO.setUserId(userId); + reqDTO.setTemplateCode(templateCode); + reqDTO.setTemplateParams(templateParams); + + notifyMessageSendApi.sendSingleMessageToMember(reqDTO); + log.debug("[站内信发送成功] userId={}, templateCode={}", userId, templateCode); + + } catch (Exception e) { + log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e); + } + } + + /** + * 发送站内信给管理员 + */ + 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); + } + } + + // ==================== 辅助方法 ==================== + + /** + * 获取区域名称 + */ + private String getAreaName(Long areaId) { + // TODO: 从区域服务获取区域名称 + return "某区域"; + } + + /** + * 记录暂停开始时间 + */ + private void recordPauseStartTime(Long orderId) { + OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); + updateObj.setOpsOrderId(orderId); + updateObj.setPauseStartTime(LocalDateTime.now()); + cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); + } +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanerStateChangeListener.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanerStateChangeListener.java deleted file mode 100644 index d275518..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanerStateChangeListener.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.viewsh.module.ops.environment.integration.listener; - -import com.viewsh.module.ops.core.event.OrderCompletedEvent; -import com.viewsh.module.ops.core.event.OrderStateChangedEvent; -import com.viewsh.module.ops.enums.WorkOrderStatusEnum; -import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService; -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.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -/** - * 保洁员状态变更监听器 - *

- * 职责:订阅工单状态变更事件,同步更新保洁员状态 - *

- * 设计说明: - * - 使用 @EventListener 订阅领域事件 - * - 业务逻辑同步执行(保证状态一致性) - * - 通过事件驱动解耦通用层与业务层 - * - 只处理保洁类型的工单(orderType = "CLEAN") - * - * @author lzh - */ -@Slf4j -@Component -public class CleanerStateChangeListener { - - @Resource - private CleanerStatusService cleanerStatusService; - - /** - * 订阅状态变更事件 - *

- * 只处理保洁类型的工单(orderType = "CLEAN") - */ - @EventListener - @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) - public void onOrderStateChanged(OrderStateChangedEvent event) { - // 只处理保洁类型的工单 - if (!"CLEAN".equals(event.getOrderType())) { - return; - } - - log.info("保洁工单状态变更: orderId={}, {} -> {}, assigneeId={}", - event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId()); - - Long assigneeId = event.getOperatorId(); - if (assigneeId == null) { - return; - } - - switch (event.getNewStatus()) { - case DISPATCHED: - // 派单:不改变保洁员状态(等待保洁员确认) - log.debug("工单已派发,等待保洁员确认: orderId={}, assigneeId={}", - event.getOrderId(), assigneeId); - break; - - case CONFIRMED: - // 确认:保洁员变更为 BUSY - updateCleanerStatus(assigneeId, com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY, - "确认工单: " + event.getOrderId()); - // 设置当前工单 - cleanerStatusService.setCurrentWorkOrder(assigneeId, event.getOrderId(), - event.getOrderCode()); - break; - - case ARRIVED: - // 到岗:保洁员保持 BUSY - updateCleanerStatus(assigneeId, com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY, - "开始作业: " + event.getOrderId()); - break; - - case PAUSED: - // 暂停:根据暂停原因决定状态 - handlePausedStatus(event, assigneeId); - break; - - case COMPLETED: - case CANCELLED: - // 完成/取消:清理当前工单,状态由 autoDispatchNext 决定 - cleanerStatusService.clearCurrentWorkOrder(assigneeId); - log.info("工单完成或取消,已清理当前工单: assigneeId={}, orderId={}", - assigneeId, event.getOrderId()); - break; - - default: - break; - } - } - - /** - * 订阅工单完成事件 - *

- * 在工单完成后自动推送下一个任务或设置保洁员为空闲 - */ - @EventListener - @Async("ops-task-executor") - @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) - public void onOrderCompleted(OrderCompletedEvent event) { - // 只处理保洁类型的工单 - if (!"CLEAN".equals(event.getOrderType())) { - return; - } - - log.info("保洁工单完成事件: orderId={}, assigneeId={}, workDuration={}秒", - event.getOrderId(), event.getAssigneeId(), event.getWorkDuration()); - - Long assigneeId = event.getAssigneeId(); - if (assigneeId == null) { - return; - } - - // 清理当前工单 - cleanerStatusService.clearCurrentWorkOrder(assigneeId); - - // 注意:状态更新(IDLE/BUSY)由 autoDispatchNextOrder 处理 - // 这里只负责清理当前工单记录 - } - - // ==================== 私有方法 ==================== - - /** - * 处理暂停状态 - */ - private void handlePausedStatus(OrderStateChangedEvent event, Long assigneeId) { - String interruptReason = event.getPayloadString("interruptReason"); - - if ("P0_TASK_INTERRUPT".equals(interruptReason)) { - // P0任务打断:释放保洁员资源 - log.warn("保洁任务被P0任务打断: orderId={}, assigneeId={}", - event.getOrderId(), assigneeId); - cleanerStatusService.clearCurrentWorkOrder(assigneeId); - // 状态保持 BUSY,因为有P0任务要处理 - } else { - // 普通暂停:保洁员变更为 PAUSED - updateCleanerStatus(assigneeId, com.viewsh.module.ops.enums.CleanerStatusEnum.PAUSED, - event.getRemark() != null ? event.getRemark() : "任务暂停"); - } - } - - /** - * 更新保洁员状态 - */ - private void updateCleanerStatus(Long userId, com.viewsh.module.ops.enums.CleanerStatusEnum newStatus, - String remark) { - try { - cleanerStatusService.updateStatus(userId, newStatus, remark); - log.info("保洁员状态已更新: userId={}, newStatus={}, remark={}", - userId, newStatus, remark); - } catch (Exception e) { - log.error("更新保洁员状态失败: userId={}, newStatus={}", userId, newStatus, e); - // 不抛出异常,避免影响主流程 - } - } -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/OrderCreatedEventListener.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/OrderCreatedEventListener.java deleted file mode 100644 index 828c71e..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/OrderCreatedEventListener.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.viewsh.module.ops.environment.integration.listener; - -import com.viewsh.module.ops.core.dispatch.DispatchEngine; -import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; -import com.viewsh.module.ops.core.event.OrderCreatedEvent; -import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; -import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; -import com.viewsh.module.ops.enums.PriorityEnum; -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; - -/** - * 工单创建事件监听器 - *

- * 监听工单创建事件,自动触发调度流程 - *

- * 职责: - * - 工单创建后自动调用调度引擎进行派单 - * - 支持 P0 紧急任务的打断逻辑(通过 CleanerPriorityScheduleStrategy) - * - * @author AI - */ -@Slf4j -@Component -public class OrderCreatedEventListener { - - @Resource - private DispatchEngine dispatchEngine; - - @Resource - private OpsOrderMapper opsOrderMapper; - - /** - * 监听工单创建事件,触发自动调度 - *

- * 使用 @TransactionalEventListener 确保在事务提交后才执行调度 - * 这样可以避免调度失败导致工单创建回滚 - */ - @org.springframework.transaction.event.TransactionalEventListener( - phase = TransactionPhase.AFTER_COMMIT - ) - public void onOrderCreated(OrderCreatedEvent event) { - try { - log.info("[OrderCreatedEventListener] 收到工单创建事件: orderId={}, orderType={}, priority={}", - event.getOrderId(), event.getOrderType(), event.getPriority()); - - // 只处理保洁工单 - if (!"CLEAN".equals(event.getOrderType())) { - log.debug("[OrderCreatedEventListener] 跳过非保洁工单: orderType={}", event.getOrderType()); - return; - } - - // 查询工单信息 - OpsOrderDO order = opsOrderMapper.selectById(event.getOrderId()); - if (order == null) { - log.warn("[OrderCreatedEventListener] 工单不存在,无法调度: orderId={}", event.getOrderId()); - return; - } - - // 构建调度上下文 - OrderDispatchContext context = OrderDispatchContext.builder() - .orderId(order.getId()) - .orderCode(order.getOrderCode()) - .orderTitle(order.getTitle()) - .businessType(order.getOrderType()) - .areaId(event.getAreaId() != null ? event.getAreaId() : order.getAreaId()) - .priority(PriorityEnum.fromPriority(event.getPriority())) - .build(); - - // 使用调度引擎执行调度(包含 P0 打断逻辑) - var result = dispatchEngine.dispatch(context); - - if (result.isSuccess()) { - log.info("[OrderCreatedEventListener] 自动调度成功: orderId={}, assigneeId={}, path={}", - event.getOrderId(), result.getAssigneeId(), result.getPath()); - } else { - log.warn("[OrderCreatedEventListener] 自动调度失败: orderId={}, reason={}", - event.getOrderId(), result.getMessage()); - } - - } catch (Exception e) { - // 调度失败不应影响工单创建 - log.error("[OrderCreatedEventListener] 自动调度异常: orderId={}", event.getOrderId(), e); - } - } -} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderExtQueryHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderExtQueryHandler.java new file mode 100644 index 0000000..fc742b4 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderExtQueryHandler.java @@ -0,0 +1,107 @@ +package com.viewsh.module.ops.environment.service.cleanorder; + +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +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.service.OrderDetailVO; +import com.viewsh.module.ops.service.OrderExtQueryHandler; +import com.viewsh.module.ops.service.OrderSummaryVO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 保洁工单扩展查询处理器 + *

+ * 实现 OrderExtQueryHandler 接口,为工单中心查询提供保洁扩展信息加载能力 + * + * @author lzh + */ +@Slf4j +@Component +public class CleanOrderExtQueryHandler implements OrderExtQueryHandler { + + @Resource + private OpsOrderCleanExtMapper cleanExtMapper; + + /** + * 支持的工单类型 + */ + private static final String ORDER_TYPE_CLEAN = "CLEAN"; + + @Override + public boolean supports(String orderType) { + return ORDER_TYPE_CLEAN.equals(orderType); + } + + @Override + public void enrichWithExtInfo(OrderSummaryVO vo, Long orderId) { + OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId); + if (cleanExt != null) { + Map extInfo = new HashMap<>(); + extInfo.put("isAuto", cleanExt.getIsAuto() != null && cleanExt.getIsAuto() == 1); + extInfo.put("expectedDuration", cleanExt.getExpectedDuration()); + extInfo.put("cleaningType", cleanExt.getCleaningType()); + extInfo.put("difficultyLevel", cleanExt.getDifficultyLevel()); + extInfo.put("arrivedTime", cleanExt.getArrivedTime()); + extInfo.put("completedTime", cleanExt.getCompletedTime()); + extInfo.put("totalPauseSeconds", cleanExt.getTotalPauseSeconds()); + vo.setExtInfo(extInfo); + } + } + + @Override + public OrderDetailVO buildDetailVO(OpsOrderDO order) { + // 查询扩展信息 + OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(order.getId()); + + // 构建基础VO + OrderDetailVO vo = OrderDetailVO.builder() + .id(order.getId()) + .orderCode(order.getOrderCode()) + .orderType(order.getOrderType()) + .sourceType(order.getSourceType()) + .title(order.getTitle()) + .description(order.getDescription()) + .priority(order.getPriority()) + .status(order.getStatus()) + .areaId(order.getAreaId()) + .location(order.getLocation()) + .urgentReason(order.getUrgentReason()) + .assigneeId(order.getAssigneeId()) + .inspectorId(order.getInspectorId()) + .startTime(order.getStartTime()) + .endTime(order.getEndTime()) + .qualityScore(order.getQualityScore()) + .qualityComment(order.getQualityComment()) + .responseSeconds(order.getResponseSeconds()) + .completionSeconds(order.getCompletionSeconds()) + .createTime(order.getCreateTime()) + .updateTime(order.getUpdateTime()) + .triggerSource(order.getTriggerSource()) + .triggerRuleId(order.getTriggerRuleId()) + .triggerDeviceId(order.getTriggerDeviceId()) + .triggerDeviceKey(order.getTriggerDeviceKey()) + .build(); + + // 填充扩展信息 + if (cleanExt != null) { + Map extInfo = new HashMap<>(); + extInfo.put("isAuto", cleanExt.getIsAuto() != null && cleanExt.getIsAuto() == 1); + extInfo.put("expectedDuration", cleanExt.getExpectedDuration()); + extInfo.put("cleaningType", cleanExt.getCleaningType()); + extInfo.put("difficultyLevel", cleanExt.getDifficultyLevel()); + extInfo.put("arrivedTime", cleanExt.getArrivedTime()); + extInfo.put("completedTime", cleanExt.getCompletedTime()); + extInfo.put("pauseStartTime", cleanExt.getPauseStartTime()); + extInfo.put("pauseEndTime", cleanExt.getPauseEndTime()); + extInfo.put("totalPauseSeconds", cleanExt.getTotalPauseSeconds()); + vo.setExtInfo(extInfo); + } + + return vo; + } +} 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 9031e26..9221ec6 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 @@ -14,7 +14,7 @@ import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqD *

* 变更说明: * - 移除了暂停/恢复/打断方法(由 {@link com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager} 处理) - * - 移除了作业时长计算方法(由 {@link com.viewsh.module.ops.environment.handler.CleanOrderEventHandler} 通过事件处理) + * - 移除了作业时长计算方法(由 {@link com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener} 通过事件处理) * * @author lzh */ 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 7f1a194..947c056 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 @@ -17,10 +17,9 @@ import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO; 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.handler.CleanOrderEventHandler; -import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService; -import com.viewsh.module.ops.environment.service.dispatch.CleanerAreaAssignStrategy; -import com.viewsh.module.ops.service.order.OpsOrderService; +import com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener; +import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator; +import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -30,23 +29,19 @@ import java.time.Duration; import java.time.LocalDateTime; /** - * 保洁工单服务实现(简化版) + * 保洁工单服务实现(重构版) *

* 职责: - * 1. 工单创建与自动分配 + * 1. 工单创建与自动分配(直接操作主表+扩展表) * 2. 保洁特有的状态转换编排(confirm, arrive, complete) * 3. 作业时长计算 *

* 架构说明: - * - 状态同步:委托给 {@link OrderLifecycleManager} - * - 派单决策:使用新的 {@link DispatchEngine} - * - 保洁员状态:由 {@link com.viewsh.module.ops.environment.integration.listener.CleanerStateChangeListener} 处理 - * - 通知逻辑:由 {@link CleanOrderEventHandler} 处理 - *

- * 变更历史: - * - Phase 3: 使用新的调度引擎和生命周期管理器 - * - Phase 3: 移除直接的状态管理代码(改为事件驱动) - * - Phase 3: 瘦身代码,专注保洁业务编排 + * - 直接操作主表和扩展表,不依赖 OpsOrderService + * - 使用公用组件 OrderIdGenerator、OrderCodeGenerator + * - 使用事件发布器 OrderEventPublisher + * - 状态同步委托给 OrderLifecycleManager + * - 派单决策使用 DispatchEngine * * @author lzh */ @@ -54,9 +49,6 @@ import java.time.LocalDateTime; @Service public class CleanOrderServiceImpl implements CleanOrderService { - @Resource - private OpsOrderService opsOrderService; - @Resource private OpsOrderMapper opsOrderMapper; @@ -64,13 +56,16 @@ public class CleanOrderServiceImpl implements CleanOrderService { private OpsOrderCleanExtMapper cleanExtMapper; @Resource - private OrderQueueService orderQueueService; + private OrderIdGenerator orderIdGenerator; -// @Resource -// private CleanerAreaAssignStrategy cleanerAreaAssignStrategy; -// -// @Resource -// private CleanerStatusService cleanerStatusService; + @Resource + private OrderCodeGenerator orderCodeGenerator; + + @Resource + private OrderEventPublisher orderEventPublisher; + + @Resource + private OrderQueueService orderQueueService; @Resource private DispatchEngine dispatchEngine; @@ -79,20 +74,40 @@ public class CleanOrderServiceImpl implements CleanOrderService { private OrderLifecycleManager orderLifecycleManager; @Resource - private CleanOrderEventHandler cleanOrderEventHandler; - - @Resource - private OrderEventPublisher orderEventPublisher; + private CleanOrderEventListener cleanOrderEventListener; // ==================== 工单创建 ==================== @Override @Transactional(rollbackFor = Exception.class) public Long createAutoCleanOrder(CleanOrderAutoCreateReqDTO createReq) { - // 1. 调用基础服务创建工单 - Long orderId = opsOrderService.createOrder(createReq); + // 1. 生成ID和编号 + Long orderId = orderIdGenerator.generate(); + String orderCode = orderCodeGenerator.generate("CLEAN"); - // 2. 创建保洁扩展信息 + // 2. 构建主表数据 + OpsOrderDO order = OpsOrderDO.builder() + .id(orderId) + .orderCode(orderCode) + .orderType("CLEAN") + .title(createReq.getTitle()) + .description(createReq.getDescription()) + .priority(createReq.getPriority() != null ? createReq.getPriority() : PriorityEnum.P2.getPriority()) + .status(WorkOrderStatusEnum.PENDING.getStatus()) + .areaId(createReq.getAreaId()) + .location(createReq.getLocation()) + .sourceType(createReq.getSourceType() != null ? createReq.getSourceType() : "TRAFFIC") + // IoT集成字段 + .triggerSource(createReq.getTriggerSource()) + .triggerRuleId(createReq.getTriggerRuleId()) + .triggerDeviceId(createReq.getTriggerDeviceId()) + .triggerDeviceKey(createReq.getTriggerDeviceKey()) + .build(); + + // 3. 插入主表 + opsOrderMapper.insert(order); + + // 4. 构建扩展表数据(同一事务) OpsOrderCleanExtDO cleanExt = OpsOrderCleanExtDO.builder() .opsOrderId(orderId) .isAuto(1) @@ -102,62 +117,29 @@ public class CleanOrderServiceImpl implements CleanOrderService { .build(); cleanExtMapper.insert(cleanExt); - log.info("创建自动保洁工单成功: orderId={}, expectedDuration={}分钟", - orderId, createReq.getExpectedDuration()); + log.info("创建自动保洁工单成功: orderId={}, orderCode={}, expectedDuration={}分钟, triggerSource={}", + orderId, orderCode, createReq.getExpectedDuration(), createReq.getTriggerSource()); - // 3. 发布工单创建事件,由 OrderCreatedEventListener 触发调度 + // 5. 发布工单创建事件,由 CleanOrderEventListener 触发调度 OrderCreatedEvent event = OrderCreatedEvent.builder() .orderId(orderId) .orderType("CLEAN") + .orderCode(orderCode) + .title(createReq.getTitle()) .areaId(createReq.getAreaId()) - .priority(PriorityEnum.fromPriority(createReq.getPriority()).getPriority()) - .createTime(java.time.LocalDateTime.now()) + .priority(order.getPriority()) + .createTime(LocalDateTime.now()) .build() .addPayload("isAuto", true) - .addPayload("expectedDuration", createReq.getExpectedDuration()); + .addPayload("expectedDuration", createReq.getExpectedDuration()) + .addPayload("triggerSource", createReq.getTriggerSource()); + orderEventPublisher.publishOrderCreated(event); return orderId; } - /** - * 自动分配工单(使用新的调度引擎) - */ - private void autoAssignOrder(Long orderId, Long areaId, PriorityEnum priority) { - try { - // 查询工单信息 - OpsOrderDO order = opsOrderMapper.selectById(orderId); - if (order == null) { - log.warn("工单不存在,无法自动分配: orderId={}", orderId); - return; - } - - // 构建调度上下文 - OrderDispatchContext context = OrderDispatchContext.builder() - .orderId(order.getId()) - .orderCode(order.getOrderCode()) - .orderTitle(order.getTitle()) - .businessType(order.getOrderType()) - .areaId(areaId) - .priority(priority) - .build(); - - // 使用调度引擎执行调度 - DispatchResult result = dispatchEngine.dispatch(context); - - if (result.isSuccess()) { - log.info("自动分配成功: orderId={}, assigneeId={}, path={}", - orderId, result.getAssigneeId(), result.getPath()); - } else { - log.warn("自动分配失败: orderId={}, reason={}", orderId, result.getMessage()); - } - - } catch (Exception e) { - log.error("自动分配工单失败: orderId={}", orderId, e); - } - } - - // ==================== 队列推送(兼容旧接口)==================== + // ==================== 队列推送 ==================== @Override @Transactional(rollbackFor = Exception.class) @@ -210,7 +192,7 @@ public class CleanOrderServiceImpl implements CleanOrderService { } // 语音播报 - cleanOrderEventHandler.sendNewOrderNotification(cleanerId, orderId); + cleanOrderEventListener.sendNewOrderNotification(cleanerId, orderId); } @Override @@ -272,7 +254,7 @@ public class CleanOrderServiceImpl implements CleanOrderService { DispatchResult result = dispatchEngine.urgentInterrupt(orderId, queueDTO.getUserId()); // 6. 发送优先级升级通知 - cleanOrderEventHandler.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode()); + cleanOrderEventListener.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode()); return result.isSuccess(); } @@ -298,7 +280,7 @@ public class CleanOrderServiceImpl implements CleanOrderService { .build(); orderLifecycleManager.transition(request); - // 注意:保洁员状态更新由 CleanerStateChangeListener 处理 + // 注意:保洁员状态更新由 CleanOrderEventListener 处理 } @Override @@ -362,7 +344,7 @@ public class CleanOrderServiceImpl implements CleanOrderService { log.info("已自动推送下一个任务: cleanerId={}", cleanerId); } else { log.info("无等待任务,保洁员变空闲: cleanerId={}", cleanerId); - // 状态更新由 CleanerStateChangeListener 处理 + // 状态更新由 CleanOrderEventListener 处理 } } @@ -370,17 +352,17 @@ public class CleanOrderServiceImpl implements CleanOrderService { @Override public void playVoiceForNewOrder(Long cleanerId) { - cleanOrderEventHandler.sendNewOrderNotification(cleanerId, null); + cleanOrderEventListener.sendNewOrderNotification(cleanerId, null); } @Override public void playVoiceForQueuedOrder(Long cleanerId, int queueCount) { - cleanOrderEventHandler.sendQueuedOrderNotification(cleanerId, queueCount); + cleanOrderEventListener.sendQueuedOrderNotification(cleanerId, queueCount); } @Override public void playVoiceForNextTask(Long cleanerId, int queueCount, String nextTaskTitle) { - cleanOrderEventHandler.sendNextTaskNotification(cleanerId, queueCount, nextTaskTitle); + cleanOrderEventListener.sendNextTaskNotification(cleanerId, queueCount, nextTaskTitle); } // ==================== 作业时长计算 ==================== @@ -403,7 +385,6 @@ public class CleanOrderServiceImpl implements CleanOrderService { /** * 记录到岗时间 - * 注意:此方法在被 @Transactional 的公共方法调用时,会参与同一事务 */ private void recordArrivedTime(Long orderId) { OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId); @@ -418,7 +399,6 @@ public class CleanOrderServiceImpl implements CleanOrderService { /** * 记录完成时间 - * 注意:此方法在被 @Transactional 的公共方法调用时,会参与同一事务 */ private void recordCompletedTime(Long orderId) { OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId); diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/CleanerPriorityScheduleStrategy.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/CleanerPriorityScheduleStrategy.java index d9a7ebe..eecf055 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/CleanerPriorityScheduleStrategy.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/CleanerPriorityScheduleStrategy.java @@ -2,6 +2,7 @@ package com.viewsh.module.ops.environment.service.dispatch; import com.viewsh.module.ops.api.queue.OrderQueueDTO; import com.viewsh.module.ops.api.queue.OrderQueueService; +import com.viewsh.module.ops.core.dispatch.DispatchEngine; import com.viewsh.module.ops.core.dispatch.model.AssigneeStatus; import com.viewsh.module.ops.core.dispatch.model.DispatchDecision; import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; @@ -46,7 +47,7 @@ public class CleanerPriorityScheduleStrategy implements ScheduleStrategy { private OrderQueueService orderQueueService; @Resource - private com.viewsh.module.ops.core.dispatch.DispatchEngine dispatchEngine; + private DispatchEngine dispatchEngine; @PostConstruct public void init() { diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderExtQueryHandlerTest.java b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderExtQueryHandlerTest.java new file mode 100644 index 0000000..5bd5faa --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderExtQueryHandlerTest.java @@ -0,0 +1,203 @@ +package com.viewsh.module.ops.environment.service.cleanorder; + +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderCleanExtDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderCleanExtMapper; +import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.service.OrderDetailVO; +import com.viewsh.module.ops.service.OrderSummaryVO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * 保洁工单扩展查询处理器测试 + * + * @author lzh + */ +@ExtendWith(MockitoExtension.class) +class CleanOrderExtQueryHandlerTest { + + @Mock + private OpsOrderCleanExtMapper cleanExtMapper; + + @InjectMocks + private CleanOrderExtQueryHandler handler; + + private OpsOrderDO testOrder; + private OpsOrderCleanExtDO testCleanExt; + + @BeforeEach + void setUp() { + // 初始化测试工单 + testOrder = OpsOrderDO.builder() + .id(1L) + .orderCode("CLEAN-20250119-0001") + .orderType("CLEAN") + .status(WorkOrderStatusEnum.PENDING.getStatus()) + .title("2楼电梯厅保洁") + .priority(PriorityEnum.P2.getPriority()) + .areaId(100L) + .location("2楼电梯厅") + .createTime(LocalDateTime.now()) + .build(); + + // 初始化测试扩展信息 + testCleanExt = OpsOrderCleanExtDO.builder() + .id(1L) + .opsOrderId(1L) + .isAuto(1) + .expectedDuration(30) + .cleaningType("DEEP") + .difficultyLevel(3) + .arrivedTime(LocalDateTime.now().minusHours(1)) + .completedTime(LocalDateTime.now()) + .totalPauseSeconds(120) + .build(); + } + + @Test + void testSupports_ReturnsTrueForClean() { + // When & Then + assertTrue(handler.supports("CLEAN")); + } + + @Test + void testSupports_ReturnsFalseForOtherTypes() { + // When & Then + assertFalse(handler.supports("SECURITY")); + assertFalse(handler.supports("FACILITIES")); + assertFalse(handler.supports("SERVICE")); + assertFalse(handler.supports(null)); + } + + @Test + void testEnrichWithExtInfo_ExistingExt_FillsExtInfo() { + // Given + when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(testCleanExt); + OrderSummaryVO vo = OrderSummaryVO.builder() + .id(1L) + .orderCode("CLEAN-20250119-0001") + .orderType("CLEAN") + .build(); + + // When + handler.enrichWithExtInfo(vo, 1L); + + // Then + assertNotNull(vo.getExtInfo()); + assertEquals(true, vo.getExtInfo().get("isAuto")); + assertEquals(30, vo.getExtInfo().get("expectedDuration")); + assertEquals("DEEP", vo.getExtInfo().get("cleaningType")); + assertEquals(3, vo.getExtInfo().get("difficultyLevel")); + assertNotNull(vo.getExtInfo().get("arrivedTime")); + assertNotNull(vo.getExtInfo().get("completedTime")); + assertEquals(120, vo.getExtInfo().get("totalPauseSeconds")); + } + + @Test + void testEnrichWithExtInfo_NoExistingExt_EmptyExtInfo() { + // Given + when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(null); + OrderSummaryVO vo = OrderSummaryVO.builder() + .id(1L) + .orderCode("CLEAN-20250119-0001") + .orderType("CLEAN") + .build(); + + // When + handler.enrichWithExtInfo(vo, 1L); + + // Then + assertNotNull(vo.getExtInfo()); + assertTrue(vo.getExtInfo().isEmpty()); + } + + @Test + void testEnrichWithExtInfo_IsAutoZero_FillsAsFalse() { + // Given + testCleanExt.setIsAuto(0); + when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(testCleanExt); + OrderSummaryVO vo = OrderSummaryVO.builder() + .id(1L) + .orderCode("CLEAN-20250119-0001") + .orderType("CLEAN") + .build(); + + // When + handler.enrichWithExtInfo(vo, 1L); + + // Then + assertEquals(false, vo.getExtInfo().get("isAuto")); + } + + @Test + void testBuildDetailVO_ExistingExt_ReturnsDetailedVO() { + // Given + when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(testCleanExt); + + // When + OrderDetailVO vo = handler.buildDetailVO(testOrder); + + // Then: 验证基础字段 + assertEquals(1L, vo.getId()); + assertEquals("CLEAN-20250119-0001", vo.getOrderCode()); + assertEquals("CLEAN", vo.getOrderType()); + assertEquals("2楼电梯厅保洁", vo.getTitle()); + assertEquals(PriorityEnum.P2.getPriority(), vo.getPriority()); + assertEquals(WorkOrderStatusEnum.PENDING.getStatus(), vo.getStatus()); + assertEquals(100L, vo.getAreaId()); + assertEquals("2楼电梯厅", vo.getLocation()); + + // 验证扩展字段 + assertNotNull(vo.getExtInfo()); + assertEquals(true, vo.getExtInfo().get("isAuto")); + assertEquals(30, vo.getExtInfo().get("expectedDuration")); + assertEquals("DEEP", vo.getExtInfo().get("cleaningType")); + assertEquals(3, vo.getExtInfo().get("difficultyLevel")); + } + + @Test + void testBuildDetailVO_NoExistingExt_ReturnsVOWithEmptyExtInfo() { + // Given + when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(null); + + // When + OrderDetailVO vo = handler.buildDetailVO(testOrder); + + // Then: 基础字段仍然存在 + assertEquals(1L, vo.getId()); + assertEquals("CLEAN-20250119-0001", vo.getOrderCode()); + + // 扩展信息为空Map + assertNotNull(vo.getExtInfo()); + assertTrue(vo.getExtInfo().isEmpty()); + } + + @Test + void testBuildDetailVO_IncludesAllExtFields() { + // Given + testCleanExt.setPauseStartTime(LocalDateTime.now().minusMinutes(30)); + testCleanExt.setPauseEndTime(LocalDateTime.now().minusMinutes(28)); + when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(testCleanExt); + + // When + OrderDetailVO vo = handler.buildDetailVO(testOrder); + + // Then: 验证所有扩展字段都被包含 + assertNotNull(vo.getExtInfo().get("arrivedTime")); + assertNotNull(vo.getExtInfo().get("completedTime")); + assertNotNull(vo.getExtInfo().get("pauseStartTime")); + assertNotNull(vo.getExtInfo().get("pauseEndTime")); + assertNotNull(vo.getExtInfo().get("totalPauseSeconds")); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManager.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManager.java index 7a8b45f..92dfb35 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManager.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManager.java @@ -58,10 +58,10 @@ public interface OrderLifecycleManager { OrderTransitionResult enqueue(OrderTransitionRequest request); /** - * 工单出队并派单:QUEUED → DISPATCHED + * 工单出队并派单:PENDING/QUEUED → DISPATCHED *

* 状态转换: - * - 工单状态:QUEUED → DISPATCHED + * - 工单状态:PENDING/QUEUED → DISPATCHED * - 队列状态:WAITING → PROCESSING * * @param request 状态转换请求 diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGenerator.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGenerator.java new file mode 100644 index 0000000..f5162f4 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGenerator.java @@ -0,0 +1,93 @@ +package com.viewsh.module.ops.infrastructure.code; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.TimeUnit; + +/** + * 工单编号生成器 + *

+ * 格式:{业务前缀}-{日期}-{序号} + * 例如:CLEAN-20250119-0001, SECURITY-20250119-0001 + *

+ * 特性: + * - 使用 Redis 保证序号唯一性 + * - 序号每日自动重置(按日期分 key) + * - 不同业务类型独立计数 + * + * @author lzh + */ +@Slf4j +@Service +public class OrderCodeGenerator { + + private static final String KEY_PREFIX = "ops:order:code:"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final long DEFAULT_EXPIRE_DAYS = 7; // key 默认保留7天 + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 生成工单编号 + * + * @param businessType 业务类型(如:CLEAN、SECURITY、FACILITIES) + * @return 工单编号,格式:{业务类型}-{日期}-{4位序号} + */ + public String generate(String businessType) { + if (businessType == null || businessType.isEmpty()) { + throw new IllegalArgumentException("Business type cannot be null or empty"); + } + + String dateStr = LocalDate.now().format(DATE_FORMATTER); + String key = KEY_PREFIX + businessType + ":" + dateStr; + + // Redis 自增并获取新值 + Long seq = stringRedisTemplate.opsForValue().increment(key); + + // 首次创建时设置过期时间 + if (seq != null && seq == 1) { + stringRedisTemplate.expire(key, DEFAULT_EXPIRE_DAYS, TimeUnit.DAYS); + } + + if (seq == null) { + throw new RuntimeException("Failed to generate order code for business type: " + businessType); + } + + String orderCode = String.format("%s-%s-%04d", businessType, dateStr, seq); + + log.debug("生成工单编号: businessType={}, orderCode={}", businessType, orderCode); + + return orderCode; + } + + /** + * 获取指定业务类型当天的当前序号 + * + * @param businessType 业务类型 + * @return 当前序号,如果不存在返回0 + */ + public long getCurrentSeq(String businessType) { + String dateStr = LocalDate.now().format(DATE_FORMATTER); + String key = KEY_PREFIX + businessType + ":" + dateStr; + String value = stringRedisTemplate.opsForValue().get(key); + return value == null ? 0 : Long.parseLong(value); + } + + /** + * 重置指定业务类型当天的序号(仅供测试或特殊场景使用) + * + * @param businessType 业务类型 + */ + public void reset(String businessType) { + String dateStr = LocalDate.now().format(DATE_FORMATTER); + String key = KEY_PREFIX + businessType + ":" + dateStr; + stringRedisTemplate.delete(key); + log.warn("重置工单编号序号: businessType={}, date={}", businessType, dateStr); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/OrderIdGenerator.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/OrderIdGenerator.java new file mode 100644 index 0000000..ad931d0 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/OrderIdGenerator.java @@ -0,0 +1,55 @@ +package com.viewsh.module.ops.infrastructure.id; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 工单ID生成器 + *

+ * 使用雪花算法,保证分布式环境下的全局唯一性。 + * 适用于工单主键生成。 + * + * @author lzh + */ +@Slf4j +@Component +public class OrderIdGenerator { + + private final OrderIdProperties orderIdProperties; + + private SnowflakeIdGenerator snowflake; + + /** + * 构造函数,注入配置 + */ + public OrderIdGenerator(OrderIdProperties orderIdProperties) { + this.orderIdProperties = orderIdProperties; + } + + /** + * 初始化雪花算法生成器 + */ + @PostConstruct + public void init() { + int datacenterId = orderIdProperties.getDatacenterId(); + int machineId = orderIdProperties.getMachineId(); + + this.snowflake = new SnowflakeIdGenerator(datacenterId, machineId); + + log.info("OrderIdGenerator初始化完成: datacenterId={}, machineId={}", + datacenterId, machineId); + } + + /** + * 生成工单ID + * + * @return 唯一的Long类型ID + */ + public Long generate() { + if (snowflake == null) { + throw new IllegalStateException("OrderIdGenerator 尚未初始化"); + } + return snowflake.nextId(); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/OrderIdProperties.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/OrderIdProperties.java new file mode 100644 index 0000000..23d2485 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/OrderIdProperties.java @@ -0,0 +1,28 @@ +package com.viewsh.module.ops.infrastructure.id; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 工单ID生成器��置 + * + * @author lzh + */ +@Data +@Component +@ConfigurationProperties(prefix = "ops.order") +public class OrderIdProperties { + + /** + * 数据中心ID(0-31) + * 默认值:1 + */ + private int datacenterId = 1; + + /** + * 机器ID(0-31) + * 默认值:1 + */ + private int machineId = 1; +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/SnowflakeIdGenerator.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/SnowflakeIdGenerator.java new file mode 100644 index 0000000..5b889aa --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/id/SnowflakeIdGenerator.java @@ -0,0 +1,184 @@ +package com.viewsh.module.ops.infrastructure.id; + +/** + * 雪花算法ID生成器 + *

+ * ID���构(64位Long): + * - 1位符号位(永远为0) + * - 41位时间戳(毫秒级,可用69年) + * - 5位数据中心ID(0-31) + * - 5位机器ID(0-31) + * - 12位序列号(毫秒内计数,0-4095) + *

+ * 特性: + * - 分布式环境全局唯一 + * - 时间有序 + * - 高性能(单机每毫秒可生成4096个ID) + * + * @author lzh + */ +public class SnowflakeIdGenerator { + + /** + * 起始时间戳(2024-01-01 00:00:00) + * 可根据项目实际情况调整 + */ + private static final long EPOCH = 1704067200000L; + + /** + * 各部分位数 + */ + private static final long DATACENTER_ID_BITS = 5L; + private static final long MACHINE_ID_BITS = 5L; + private static final long SEQUENCE_BITS = 12L; + + /** + * 各部分最大值 + */ + private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); // 31 + private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS); // 31 + private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095 + + /** + * 各部分位移 + */ + private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS; + private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS; + private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATACENTER_ID_BITS; + + /** + * 数据中心ID + */ + private final long datacenterId; + + /** + * 机器ID + */ + private final long machineId; + + /** + * 序列号 + */ + private long sequence = 0L; + + /** + * 上次生成ID的时间戳 + */ + private long lastTimestamp = -1L; + + /** + * 构造函数 + * + * @param datacenterId 数据中心ID(0-31) + * @param machineId 机器ID(0-31) + * @throws IllegalArgumentException 如果ID超出范围 + */ + public SnowflakeIdGenerator(long datacenterId, long machineId) { + if (datacenterId < 0 || datacenterId > MAX_DATACENTER_ID) { + throw new IllegalArgumentException( + String.format("Datacenter ID must be between 0 and %d", MAX_DATACENTER_ID)); + } + if (machineId < 0 || machineId > MAX_MACHINE_ID) { + throw new IllegalArgumentException( + String.format("Machine ID must be between 0 and %d", MAX_MACHINE_ID)); + } + this.datacenterId = datacenterId; + this.machineId = machineId; + } + + /** + * 生成下一个ID(线程安全) + * + * @return 唯一的Long类型ID + */ + public synchronized long nextId() { + long timestamp = getCurrentTimestamp(); + + // 时钟回拨检查 + if (timestamp < lastTimestamp) { + long offset = lastTimestamp - timestamp; + if (offset <= 5) { + // 少量时钟回拨,等待 + try { + Thread.sleep(offset << 1); + timestamp = getCurrentTimestamp(); + if (timestamp < lastTimestamp) { + throw new RuntimeException("Clock moved backwards. Refusing to generate ID"); + } + } catch (InterruptedException e) { + throw new RuntimeException("Clock moved backwards. Waiting interrupted", e); + } + } else { + throw new RuntimeException("Clock moved backwards. Refusing to generate ID"); + } + } + + // 同一毫秒内,序列号自增 + if (timestamp == lastTimestamp) { + sequence = (sequence + 1) & MAX_SEQUENCE; + // 序列号溢出,等待下一毫秒 + if (sequence == 0) { + timestamp = tilNextMillis(lastTimestamp); + } + } else { + // 新的毫秒,序列号重置 + sequence = 0L; + } + + lastTimestamp = timestamp; + + // 组装ID + return ((timestamp - EPOCH) << TIMESTAMP_SHIFT) + | (datacenterId << DATACENTER_ID_SHIFT) + | (machineId << MACHINE_ID_SHIFT) + | sequence; + } + + /** + * 获取当前时间戳 + */ + private long getCurrentTimestamp() { + return System.currentTimeMillis(); + } + + /** + * 等待下一毫秒 + */ + private long tilNextMillis(long lastTimestamp) { + long timestamp = getCurrentTimestamp(); + while (timestamp <= lastTimestamp) { + timestamp = getCurrentTimestamp(); + } + return timestamp; + } + + /** + * 解析ID,获取时间戳信息 + * + * @param id 雪花算法生成的ID + * @return 原始时间戳(毫秒) + */ + public static long parseTimestamp(long id) { + return ((id >> TIMESTAMP_SHIFT) & ~(-1L << 41L)) + EPOCH; + } + + /** + * 解析ID,获取数据中心ID + * + * @param id 雪花算法生成的ID + * @return 数据中心ID + */ + public static long parseDatacenterId(long id) { + return (id >> DATACENTER_ID_SHIFT) & MAX_DATACENTER_ID; + } + + /** + * 解析ID,获取机器ID + * + * @param id 雪花算法生成的ID + * @return 机器ID + */ + public static long parseMachineId(long id) { + return (id >> MACHINE_ID_SHIFT) & MAX_MACHINE_ID; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderDetailVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderDetailVO.java new file mode 100644 index 0000000..1614f1d --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderDetailVO.java @@ -0,0 +1,164 @@ +package com.viewsh.module.ops.service; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 工单详情 VO(基类) + *

+ * 支持多态,具体业务类型可继承此类添加扩展字段 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderDetailVO { + + /** + * 工单ID + */ + private Long id; + + /** + * 工单编号 + */ + private String orderCode; + + /** + * 工单类型 + */ + private String orderType; + + /** + * 来源类型 + */ + private String sourceType; + + /** + * 工单标题 + */ + private String title; + + /** + * 工单描述 + */ + private String description; + + /** + * 优先级 + */ + private Integer priority; + + /** + * 工单状态 + */ + private String status; + + /** + * 区域ID + */ + private Long areaId; + + /** + * 具体位置描述 + */ + private String location; + + /** + * 加急原因 + */ + private String urgentReason; + + /** + * 当前执行人ID + */ + private Long assigneeId; + + /** + * 当前执行人姓名 + */ + private String assigneeName; + + /** + * 巡检员ID + */ + private Long inspectorId; + + /** + * 巡检员姓名 + */ + private String inspectorName; + + /** + * 工单开始时间 + */ + private LocalDateTime startTime; + + /** + * 工单结束时间 + */ + private LocalDateTime endTime; + + /** + * 验收评分 + */ + private Integer qualityScore; + + /** + * 验收评语 + */ + private String qualityComment; + + /** + * 响应耗时(秒) + */ + private Integer responseSeconds; + + /** + * 完成耗时(秒) + */ + private Integer completionSeconds; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; + + /** + * 触发来源 + */ + private String triggerSource; + + /** + * 触发规则ID + */ + private Long triggerRuleId; + + /** + * 触发设备ID + */ + private Long triggerDeviceId; + + /** + * 触发设备Key + */ + private String triggerDeviceKey; + + /** + * 扩展信息(Map形式,通用字段) + */ + @Builder.Default + private Map extInfo = new java.util.HashMap<>(); +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderExtQueryHandler.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderExtQueryHandler.java new file mode 100644 index 0000000..aac2303 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderExtQueryHandler.java @@ -0,0 +1,46 @@ +package com.viewsh.module.ops.service; + +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; + +import java.util.Map; + +/** + * 工单扩展表查询处理器接口 + *

+ * 各业务模块实现此接口,提供对应业务类型的扩展信息加载能力。 + * 例如: + * - environment-biz 实现 CleanOrderExtQueryHandler + * - security-biz 实现 SecurityOrderExtQueryHandler + * + * @author lzh + */ +public interface OrderExtQueryHandler { + + /** + * 是否支持此业务类型 + * + * @param orderType 工单类型(如:CLEAN、SECURITY、FACILITIES) + * @return true-支持,false-不支持 + */ + boolean supports(String orderType); + + /** + * 为汇总VO填充扩展信息 + *

+ * 用于分页查询场景,将扩展信息填充到 extInfo Map 中 + * + * @param vo 汇总VO + * @param orderId 工单ID + */ + void enrichWithExtInfo(OrderSummaryVO vo, Long orderId); + + /** + * 构建详情VO + *

+ * 用于详情查询场景,返回包含完整扩展信息的详情VO + * + * @param order 工单主表数据 + * @return 详情VO + */ + OrderDetailVO buildDetailVO(OpsOrderDO order); +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderQueryService.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderQueryService.java new file mode 100644 index 0000000..109b250 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderQueryService.java @@ -0,0 +1,220 @@ +package com.viewsh.module.ops.service; + +import com.viewsh.framework.common.pojo.PageResult; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; + +import java.util.Map; + +/** + * 工单中心查询服务接口 + *

+ * 职责: + * 1. 综合分页查询(支持所有业务类型) + * 2. 工单详情查询(含完整扩展信息) + * 3. 工单统计信息 + * + * @author lzh + */ +public interface OrderQueryService { + + /** + * 分页查询工单(支持所有业务类型) + *

+ * 查询主表数据后,自动调用对应的扩展查询处理器填充扩展信息 + * + * @param query 查询条件 + * @return 分页结果 + */ + PageResult queryPage(OrderQuery query); + + /** + * 查询工单详情(含完整扩展信息) + *

+ * 根据工单的业务类型,调用对应的扩展查询处理器加载完整扩展信息 + * + * @param orderId 工单ID + * @return 详情VO + */ + OrderDetailVO getDetail(Long orderId); + + /** + * 获取工单统计信息 + *

+ * 按业务类型分组返回工单数量统计 + * + * @param groupBy 分组维度(status:按状态统计,null:只按类型统计) + * @return 统计结果 + */ + Map getStats(String groupBy); + + /** + * 工单查询条件 + */ + class OrderQuery { + /** + * 页码(从1开始) + */ + private Integer page = 1; + + /** + * 每页大小 + */ + private Integer size = 20; + + /** + * 工单类型(可选) + */ + private String orderType; + + /** + * 工单状态(可选) + */ + private String status; + + /** + * 优先级(可选) + */ + private Integer priority; + + /** + * 区域ID(可选) + */ + private Long areaId; + + /** + * 执行人ID(可选) + */ + private Long assigneeId; + + /** + * 工单编号模糊查询(可选) + */ + private String orderCode; + + /** + * 标题模糊查询(可选) + */ + private String title; + + public Integer getPage() { + return page; + } + + public void setPage(Integer page) { + this.page = page; + } + + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } + + public String getOrderType() { + return orderType; + } + + public void setOrderType(String orderType) { + this.orderType = orderType; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public Long getAreaId() { + return areaId; + } + + public void setAreaId(Long areaId) { + this.areaId = areaId; + } + + public Long getAssigneeId() { + return assigneeId; + } + + public void setAssigneeId(Long assigneeId) { + this.assigneeId = assigneeId; + } + + public String getOrderCode() { + return orderCode; + } + + public void setOrderCode(String orderCode) { + this.orderCode = orderCode; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public static OrderQuery builder() { + return new OrderQuery(); + } + + public OrderQuery withPage(Integer page) { + this.page = page; + return this; + } + + public OrderQuery withSize(Integer size) { + this.size = size; + return this; + } + + public OrderQuery withOrderType(String orderType) { + this.orderType = orderType; + return this; + } + + public OrderQuery withStatus(String status) { + this.status = status; + return this; + } + + public OrderQuery withPriority(Integer priority) { + this.priority = priority; + return this; + } + + public OrderQuery withAreaId(Long areaId) { + this.areaId = areaId; + return this; + } + + public OrderQuery withAssigneeId(Long assigneeId) { + this.assigneeId = assigneeId; + return this; + } + + public OrderQuery withOrderCode(String orderCode) { + this.orderCode = orderCode; + return this; + } + + public OrderQuery withTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderQueryServiceImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderQueryServiceImpl.java new file mode 100644 index 0000000..cfd765d --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderQueryServiceImpl.java @@ -0,0 +1,186 @@ +package com.viewsh.module.ops.service; + +import com.viewsh.framework.common.pojo.PageResult; +import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 工单中心查询服务实现 + * + * @author lzh + */ +@Slf4j +@Service +public class OrderQueryServiceImpl implements OrderQueryService { + + @Resource + private OpsOrderMapper opsOrderMapper; + + /** + * 扩展查询处理器列表(由各业务模块注入) + */ + private final List extQueryHandlers; + + public OrderQueryServiceImpl(List extQueryHandlers) { + this.extQueryHandlers = extQueryHandlers; + log.info("OrderQueryService 初始化,注册 {} 个扩展查询处理器", extQueryHandlers.size()); + } + + @Override + public PageResult queryPage(OrderQuery query) { + // 1. 构建查询条件 + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eqIfPresent(OpsOrderDO::getOrderType, query.getOrderType()) + .eqIfPresent(OpsOrderDO::getStatus, query.getStatus()) + .eqIfPresent(OpsOrderDO::getPriority, query.getPriority()) + .eqIfPresent(OpsOrderDO::getAreaId, query.getAreaId()) + .eqIfPresent(OpsOrderDO::getAssigneeId, query.getAssigneeId()) + .likeIfPresent(OpsOrderDO::getOrderCode, query.getOrderCode()) + .likeIfPresent(OpsOrderDO::getTitle, query.getTitle()) + .orderByDesc(OpsOrderDO::getCreateTime); + + // 2. 分页查询主表 + PageResult pageResult = opsOrderMapper.selectPage(query.getPage(), query.getSize(), wrapper); + + // 3. ���换为 VO 并填充扩展信息 + List voList = pageResult.getList().stream() + .map(this::convertToSummaryVO) + .toList(); + + return new PageResult<>(voList, pageResult.getTotal()); + } + + @Override + public OrderDetailVO getDetail(Long orderId) { + // 1. 查询主表 + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + throw new RuntimeException("工单不存在: " + orderId); + } + + // 2. 找到对应的扩展查询处理器 + OrderExtQueryHandler handler = findExtQueryHandler(order.getOrderType()); + + // 3. 构建详情VO + if (handler != null) { + return handler.buildDetailVO(order); + } else { + // 没有对应的扩展查询处理器,返回基础VO + return convertToDetailVO(order); + } + } + + @Override + public Map getStats(String groupBy) { + Map result = new HashMap<>(); + + if ("status".equals(groupBy)) { + // 按类型和状态统计 + List allOrders = opsOrderMapper.selectList(); + Map> typeStatusMap = new HashMap<>(); + + for (OpsOrderDO order : allOrders) { + String type = order.getOrderType(); + String status = order.getStatus(); + + typeStatusMap.computeIfAbsent(type, k -> new HashMap<>()) + .merge(status, 1L, Long::sum); + } + + result.putAll(typeStatusMap); + } else { + // 只按类型统计 + List allOrders = opsOrderMapper.selectList(); + Map typeCountMap = new HashMap<>(); + + for (OpsOrderDO order : allOrders) { + String type = order.getOrderType(); + typeCountMap.merge(type, 1L, Long::sum); + } + + result.putAll(typeCountMap); + } + + return result; + } + + /** + * 转换为汇总VO并填充扩展信息 + */ + private OrderSummaryVO convertToSummaryVO(OpsOrderDO order) { + OrderSummaryVO vo = OrderSummaryVO.builder() + .id(order.getId()) + .orderCode(order.getOrderCode()) + .orderType(order.getOrderType()) + .title(order.getTitle()) + .description(order.getDescription()) + .priority(order.getPriority()) + .status(order.getStatus()) + .areaId(order.getAreaId()) + .location(order.getLocation()) + .assigneeId(order.getAssigneeId()) + .startTime(order.getStartTime()) + .endTime(order.getEndTime()) + .createTime(order.getCreateTime()) + .build(); + + // 填充扩展信息 + OrderExtQueryHandler handler = findExtQueryHandler(order.getOrderType()); + if (handler != null) { + handler.enrichWithExtInfo(vo, order.getId()); + } + + return vo; + } + + /** + * 转换为详情VO(基础版本,不含扩展信息) + */ + private OrderDetailVO convertToDetailVO(OpsOrderDO order) { + return OrderDetailVO.builder() + .id(order.getId()) + .orderCode(order.getOrderCode()) + .orderType(order.getOrderType()) + .sourceType(order.getSourceType()) + .title(order.getTitle()) + .description(order.getDescription()) + .priority(order.getPriority()) + .status(order.getStatus()) + .areaId(order.getAreaId()) + .location(order.getLocation()) + .urgentReason(order.getUrgentReason()) + .assigneeId(order.getAssigneeId()) + .inspectorId(order.getInspectorId()) + .startTime(order.getStartTime()) + .endTime(order.getEndTime()) + .qualityScore(order.getQualityScore()) + .qualityComment(order.getQualityComment()) + .responseSeconds(order.getResponseSeconds()) + .completionSeconds(order.getCompletionSeconds()) + .createTime(order.getCreateTime()) + .updateTime(order.getUpdateTime()) + .triggerSource(order.getTriggerSource()) + .triggerRuleId(order.getTriggerRuleId()) + .triggerDeviceId(order.getTriggerDeviceId()) + .triggerDeviceKey(order.getTriggerDeviceKey()) + .build(); + } + + /** + * 查找对应的扩展查询处理器 + */ + private OrderExtQueryHandler findExtQueryHandler(String orderType) { + return extQueryHandlers.stream() + .filter(handler -> handler.supports(orderType)) + .findFirst() + .orElse(null); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderSummaryVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderSummaryVO.java new file mode 100644 index 0000000..5d71509 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/OrderSummaryVO.java @@ -0,0 +1,103 @@ +package com.viewsh.module.ops.service; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 工单汇总信息 VO + *

+ * 用于工单中心分页查询,包含主表信息和扩展信息(Map形式) + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderSummaryVO { + + /** + * 工单ID + */ + private Long id; + + /** + * 工单编号 + */ + private String orderCode; + + /** + * 工单类型(CLEAN/SECURITY/FACILITIES/SERVICE) + */ + private String orderType; + + /** + * 工单标题 + */ + private String title; + + /** + * 工单描述 + */ + private String description; + + /** + * 优先级(0=P0/1=P1/2=P2) + */ + private Integer priority; + + /** + * 工单状态 + */ + private String status; + + /** + * 区域ID + */ + private Long areaId; + + /** + * 具体位置描述 + */ + private String location; + + /** + * 当前执行人ID + */ + private Long assigneeId; + + /** + * 当前执行人姓名(冗余) + */ + private String assigneeName; + + /** + * 工单开始时间 + */ + private LocalDateTime startTime; + + /** + * 工单结束时间 + */ + private LocalDateTime endTime; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 扩展信息(Map形式) + *

+ * 根据工单类型不同,包含不同的扩展字段: + * - CLEAN: expectedDuration, cleaningType, difficultyLevel + * - SECURITY: route, checkpoint, patrolTime + */ + @Builder.Default + private Map extInfo = new java.util.HashMap<>(); +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGeneratorTest.java b/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGeneratorTest.java new file mode 100644 index 0000000..80d5998 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGeneratorTest.java @@ -0,0 +1,182 @@ +package com.viewsh.module.ops.infrastructure.code; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * 工单编号生成器测试 + * + * @author lzh + */ +@ExtendWith(MockitoExtension.class) +class OrderCodeGeneratorTest { + + @Mock + private StringRedisTemplate stringRedisTemplate; + + @Mock + private ValueOperations valueOperations; + + private OrderCodeGenerator orderCodeGenerator; + + private static final String DATE_FORMAT = "yyyyMMdd"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT); + + @BeforeEach + void setUp() { + orderCodeGenerator = new OrderCodeGenerator(); + ReflectionTestUtils.setField(orderCodeGenerator, "stringRedisTemplate", stringRedisTemplate); + + // Mock stringRedisTemplate.opsForValue() + lenient().when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); + } + + @Test + void testGenerate_FirstOrderOfToday() { + // Given + when(valueOperations.increment(anyString())).thenReturn(1L); + + // When + String orderCode = orderCodeGenerator.generate("CLEAN"); + + // Then + String expectedDate = LocalDate.now().format(DATE_FORMAT); + String expected = String.format("CLEAN-%s-0001", expectedDate); + assertEquals(expected, orderCode); + } + + @Test + void testGenerate_SecondOrderOfToday() { + // Given + when(valueOperations.increment(anyString())).thenReturn(2L); + + // When + String orderCode = orderCodeGenerator.generate("CLEAN"); + + // Then + String expectedDate = LocalDate.now().format(DATE_FORMAT); + String expected = String.format("CLEAN-%s-0002", expectedDate); + assertEquals(expected, orderCode); + } + + @Test + void testGenerate_DifferentBusinessTypes() { + // Given + when(valueOperations.increment(anyString())).thenReturn(1L); + + // When + String cleanCode = orderCodeGenerator.generate("CLEAN"); + String securityCode = orderCodeGenerator.generate("SECURITY"); + + // Then + assertTrue(cleanCode.startsWith("CLEAN-")); + assertTrue(securityCode.startsWith("SECURITY-")); + assertNotEquals(cleanCode, securityCode); + } + + @Test + void testGenerate_LargeSequenceNumber() { + // Given + when(valueOperations.increment(anyString())).thenReturn(9999L); + + // When + String orderCode = orderCodeGenerator.generate("CLEAN"); + + // Then + String expectedDate = LocalDate.now().format(DATE_FORMAT); + String expected = String.format("CLEAN-%s-9999", expectedDate); + assertEquals(expected, orderCode); + } + + @Test + void testGenerate_NullBusinessType_ThrowsException() { + // When & Then + assertThrows(IllegalArgumentException.class, () -> orderCodeGenerator.generate(null)); + } + + @Test + void testGenerate_EmptyBusinessType_ThrowsException() { + // When & Then + assertThrows(IllegalArgumentException.class, () -> orderCodeGenerator.generate("")); + } + + @Test + void testGenerate_SetsExpirationOnFirstUse() { + // Given + when(valueOperations.increment(anyString())).thenReturn(1L); + + // When + orderCodeGenerator.generate("CLEAN"); + + // Then + verify(stringRedisTemplate).expire(anyString(), eq(7L), any()); + } + + @Test + void testGetCurrentSeq_NoExistingRecords_ReturnsZero() { + // Given + when(valueOperations.get(anyString())).thenReturn(null); + + // When + long seq = orderCodeGenerator.getCurrentSeq("CLEAN"); + + // Then + assertEquals(0, seq); + } + + @Test + void testGetCurrentSeq_ExistingRecords_ReturnsValue() { + // Given + when(valueOperations.get(anyString())).thenReturn("5"); + + // When + long seq = orderCodeGenerator.getCurrentSeq("CLEAN"); + + // Then + assertEquals(5, seq); + } + + @Test + void testReset_DeletesKey() { + // When + orderCodeGenerator.reset("CLEAN"); + + // Then + verify(stringRedisTemplate).delete(contains("CLEAN")); + } + + @Test + void testGenerate_FormatConsistency() { + // Given + when(valueOperations.increment(anyString())).thenReturn(1L); + + // When + String orderCode = orderCodeGenerator.generate("CLEAN"); + + // Then: 验证格式为 {TYPE}-{DATE}-{SEQUENCE} + String[] parts = orderCode.split("-"); + assertEquals(3, parts.length); + assertEquals("CLEAN", parts[0]); + + // 验证日期部分是8位数字 + assertEquals(8, parts[1].length()); + assertTrue(parts[1].matches("\\d{8}")); + + // 验证序号部分是4位数字 + assertEquals(4, parts[2].length()); + assertTrue(parts[2].matches("\\d{4}")); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/infrastructure/id/SnowflakeIdGeneratorTest.java b/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/infrastructure/id/SnowflakeIdGeneratorTest.java new file mode 100644 index 0000000..35cb138 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/infrastructure/id/SnowflakeIdGeneratorTest.java @@ -0,0 +1,178 @@ +package com.viewsh.module.ops.infrastructure.id; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 雪花算法ID生成器测试 + * + * @author lzh + */ +class SnowflakeIdGeneratorTest { + + private SnowflakeIdGenerator snowflake; + + @BeforeEach + void setUp() { + snowflake = new SnowflakeIdGenerator(1, 1); + } + + @Test + void testNextId_ReturnsUniqueId() { + // When + Long id1 = snowflake.nextId(); + Long id2 = snowflake.nextId(); + + // Then + assertNotNull(id1); + assertNotNull(id2); + assertNotEquals(id1, id2); + assertTrue(id2 > id1, "ID应该递增"); + } + + @Test + void testNextId_GeneratesIncreasingIds() { + // Given + Long previousId = 0L; + + // When: 生成100个ID + for (int i = 0; i < 100; i++) { + Long currentId = snowflake.nextId(); + + // Then + assertTrue(currentId > previousId, "ID应该严格递增"); + previousId = currentId; + } + } + + @Test + void testConstructor_ValidDatacenterId_Success() { + // When & Then: 测试边界值 + assertDoesNotThrow(() -> new SnowflakeIdGenerator(0, 0)); + assertDoesNotThrow(() -> new SnowflakeIdGenerator(31, 31)); + } + + @Test + void testConstructor_InvalidDatacenterId_ThrowsException() { + // When & Then + assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(-1, 1)); + assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(32, 1)); + } + + @Test + void testConstructor_InvalidMachineId_ThrowsException() { + // When & Then + assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(1, -1)); + assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(1, 32)); + } + + @Test + void testNextId_DifferentDatacenterOrMachine_GeneratesDifferentIds() { + // Given + SnowflakeIdGenerator generator1 = new SnowflakeIdGenerator(1, 1); + SnowflakeIdGenerator generator2 = new SnowflakeIdGenerator(1, 2); + SnowflakeIdGenerator generator3 = new SnowflakeIdGenerator(2, 1); + + // When + Long id1 = generator1.nextId(); + Long id2 = generator2.nextId(); + Long id3 = generator3.nextId(); + + // Then: 不同数据中心或机器生成的ID应该不同 + assertNotEquals(id1, id2); + assertNotEquals(id1, id3); + assertNotEquals(id2, id3); + } + + @Test + void testNextId_Concurrent_ThreadSafe() throws InterruptedException { + // Given + int threadCount = 10; + int idsPerThread = 1000; + Set allIds = Collections.newSetFromMap(new ConcurrentHashMap<>()); + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + // When: 多线程并发生成ID + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + for (int j = 0; j < idsPerThread; j++) { + Long id = snowflake.nextId(); + boolean isNew = allIds.add(id); + assertTrue(isNew, "并发生成ID应该唯一"); + } + }); + } + + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + + // Then: 验证ID唯一性 + assertEquals(threadCount * idsPerThread, allIds.size()); + } + + @RepeatedTest(10) + void testNextId_HighThroughput_NoDuplicateInSameMillisecond() { + // Given + Set ids = new HashSet<>(); + + // When: 快速生成100个ID + for (int i = 0; i < 100; i++) { + ids.add(snowflake.nextId()); + } + + // Then: 同一毫秒内应该生成不同的ID + assertEquals(100, ids.size()); + } + + @Test + void testParseTimestamp() { + // Given + Long id = snowflake.nextId(); + long beforeParse = System.currentTimeMillis(); + + // When + long timestamp = SnowflakeIdGenerator.parseTimestamp(id); + long afterParse = System.currentTimeMillis(); + + // Then: 解析的时间戳应该接近当前时间 + assertTrue(timestamp >= beforeParse - 1000, "时间戳应该接近当前时间"); + assertTrue(timestamp <= afterParse + 1000, "时间戳应该接近当前时间"); + } + + @Test + void testParseDatacenterId() { + // Given + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(5, 10); + Long id = generator.nextId(); + + // When + long datacenterId = SnowflakeIdGenerator.parseDatacenterId(id); + + // Then + assertEquals(5, datacenterId); + } + + @Test + void testParseMachineId() { + // Given + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(5, 10); + Long id = generator.nextId(); + + // When + long machineId = SnowflakeIdGenerator.parseMachineId(id); + + // Then + assertEquals(10, machineId); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/OrderCenterController.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/OrderCenterController.java new file mode 100644 index 0000000..12527a1 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/OrderCenterController.java @@ -0,0 +1,93 @@ +package com.viewsh.module.ops.controller.admin; + +import com.viewsh.framework.common.pojo.CommonResult; +import com.viewsh.framework.common.pojo.PageResult; +import com.viewsh.module.ops.service.OrderDetailVO; +import com.viewsh.module.ops.service.OrderQueryService; +import com.viewsh.module.ops.service.OrderSummaryVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +import static com.viewsh.framework.common.pojo.CommonResult.success; + +/** + * 管理后台 - 工单中心 Controller + *

+ * 工单中心提供综合查询能力,支持所有业务类型的工单查询 + * + * @author lzh + */ +@Tag(name = "管理后台 - 工单中心") +@Slf4j +@RestController +@RequestMapping("/ops/order-center") +@Validated +public class OrderCenterController { + + @Resource + private OrderQueryService orderQueryService; + + @GetMapping("/page") + @Operation(summary = "工单中心分页查询") + @Parameter(name = "page", description = "页码", required = false) + @Parameter(name = "size", description = "每页大小", required = false) + @Parameter(name = "orderType", description = "工单类型", required = false) + @Parameter(name = "status", description = "工单状态", required = false) + @Parameter(name = "priority", description = "优先级", required = false) + @Parameter(name = "areaId", description = "区域ID", required = false) + @Parameter(name = "assigneeId", description = "执行人ID", required = false) + @PreAuthorize("@ss.hasPermission('ops:order-center:query')") + public CommonResult> queryPage( + @RequestParam(value = "page", required = false, defaultValue = "1") Integer page, + @RequestParam(value = "size", required = false, defaultValue = "20") Integer size, + @RequestParam(value = "orderType", required = false) String orderType, + @RequestParam(value = "status", required = false) String status, + @RequestParam(value = "priority", required = false) Integer priority, + @RequestParam(value = "areaId", required = false) Long areaId, + @RequestParam(value = "assigneeId", required = false) Long assigneeId, + @RequestParam(value = "orderCode", required = false) String orderCode, + @RequestParam(value = "title", required = false) String title) { + + OrderQueryService.OrderQuery query = OrderQueryService.OrderQuery.builder() + .withPage(page) + .withSize(size) + .withOrderType(orderType) + .withStatus(status) + .withPriority(priority) + .withAreaId(areaId) + .withAssigneeId(assigneeId) + .withOrderCode(orderCode) + .withTitle(title); + + PageResult result = orderQueryService.queryPage(query); + return success(result); + } + + @GetMapping("/detail/{id}") + @Operation(summary = "工单详情查询") + @Parameter(name = "id", description = "工单ID", required = true) + @PreAuthorize("@ss.hasPermission('ops:order-center:query')") + public CommonResult getDetail(@PathVariable("id") Long id) { + OrderDetailVO detail = orderQueryService.getDetail(id); + return success(detail); + } + + @GetMapping("/stats") + @Operation(summary = "工单统计") + @Parameter(name = "groupBy", description = "分组维度(status:按状态统计)", required = false) + @PreAuthorize("@ss.hasPermission('ops:order-center:query')") + public CommonResult> getStats( + @RequestParam(value = "groupBy", required = false) String groupBy) { + + Map stats = orderQueryService.getStats(groupBy); + return success(stats); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/resources/application.yaml b/viewsh-module-ops/viewsh-module-ops-server/src/main/resources/application.yaml index 846ce7e..f45ac80 100644 --- a/viewsh-module-ops/viewsh-module-ops-server/src/main/resources/application.yaml +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/resources/application.yaml @@ -140,4 +140,11 @@ viewsh: tenant: # 多租户相关配置项 enable: true +--- #################### 工单相关配置 #################### + +ops: + order: + datacenter-id: 1 # 数据中心ID(0-31) + machine-id: 1 # 机器ID(0-31) + debug: false diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/test/java/com/viewsh/module/ops/controller/admin/OrderCenterControllerTest.java b/viewsh-module-ops/viewsh-module-ops-server/src/test/java/com/viewsh/module/ops/controller/admin/OrderCenterControllerTest.java new file mode 100644 index 0000000..13c8432 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-server/src/test/java/com/viewsh/module/ops/controller/admin/OrderCenterControllerTest.java @@ -0,0 +1,201 @@ +package com.viewsh.module.ops.controller.admin; + +import com.viewsh.framework.common.pojo.CommonResult; +import com.viewsh.framework.common.pojo.PageResult; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.service.OrderDetailVO; +import com.viewsh.module.ops.service.OrderQueryService; +import com.viewsh.module.ops.service.OrderSummaryVO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * 工单中心控制器测试 + * + * @author lzh + */ +@ExtendWith(MockitoExtension.class) +class OrderCenterControllerTest { + + @Mock + private OrderQueryService orderQueryService; + + @InjectMocks + private OrderCenterController controller; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void testQueryPage_Success() { + // Given + OrderSummaryVO vo = OrderSummaryVO.builder() + .id(1L) + .orderCode("CLEAN-20250119-0001") + .orderType("CLEAN") + .title("2楼电梯厅保洁") + .status(WorkOrderStatusEnum.PENDING.getStatus()) + .priority(PriorityEnum.P2.getPriority()) + .areaId(100L) + .createTime(LocalDateTime.now()) + .extInfo(new HashMap<>()) + .build(); + + PageResult pageResult = new PageResult<>(List.of(vo), 1L); + + when(orderQueryService.queryPage(any())).thenReturn(pageResult); + + // When + CommonResult> result = controller.queryPage( + 1, 20, "CLEAN", null, null, null, null, null, null); + + // Then + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + assertEquals(1, result.getData().getList().size()); + assertEquals("CLEAN-20250119-0001", result.getData().getList().get(0).getOrderCode()); + + verify(orderQueryService, times(1)).queryPage(any()); + } + + @Test + void testQueryPage_WithMultipleFilters() { + // Given + PageResult emptyResult = new PageResult<>(List.of(), 0L); + when(orderQueryService.queryPage(any())).thenReturn(emptyResult); + + // When + controller.queryPage(1, 20, "CLEAN", "PENDING", + PriorityEnum.P2.getPriority(), 100L, null, null, null); + + // Then + verify(orderQueryService, times(1)).queryPage(argThat(query -> + query.getOrderType().equals("CLEAN") && + query.getStatus().equals("PENDING") && + query.getPriority().equals(PriorityEnum.P2.getPriority()) && + query.getAreaId().equals(100L) + )); + } + + @Test + void testGetDetail_Success() { + // Given + OrderDetailVO vo = OrderDetailVO.builder() + .id(1L) + .orderCode("CLEAN-20250119-0001") + .orderType("CLEAN") + .title("2楼电梯厅保洁") + .status(WorkOrderStatusEnum.PENDING.getStatus()) + .priority(PriorityEnum.P2.getPriority()) + .areaId(100L) + .extInfo(new HashMap<>()) + .build(); + + when(orderQueryService.getDetail(1L)).thenReturn(vo); + + // When + CommonResult result = controller.getDetail(1L); + + // Then + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + assertEquals(1L, result.getData().getId()); + assertEquals("CLEAN-20250119-0001", result.getData().getOrderCode()); + + verify(orderQueryService, times(1)).getDetail(1L); + } + + @Test + void testGetDetail_NotFound_ThrowsException() { + // Given + when(orderQueryService.getDetail(999L)) + .thenThrow(new RuntimeException("工单不存在")); + + // When & Then + assertThrows(RuntimeException.class, () -> controller.getDetail(999L)); + + verify(orderQueryService, times(1)).getDetail(999L); + } + + @Test + void testGetStats_Success() { + // Given + Map stats = new HashMap<>(); + stats.put("CLEAN", 50L); + stats.put("SECURITY", 30L); + + when(orderQueryService.getStats(null)).thenReturn(stats); + + // When + CommonResult> result = controller.getStats(null); + + // Then + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + assertEquals(50L, result.getData().get("CLEAN")); + assertEquals(30L, result.getData().get("SECURITY")); + + verify(orderQueryService, times(1)).getStats(null); + } + + @Test + void testGetStats_WithGroupByStatus() { + // Given + Map stats = new HashMap<>(); + Map cleanStatus = new HashMap<>(); + cleanStatus.put("PENDING", 10L); + cleanStatus.put("COMPLETED", 20L); + stats.put("CLEAN", cleanStatus); + + when(orderQueryService.getStats("status")).thenReturn(stats); + + // When + CommonResult> result = controller.getStats("status"); + + // Then + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + assertNotNull(result.getData().get("CLEAN")); + + verify(orderQueryService, times(1)).getStats("status"); + } + + @Test + void testQueryPage_DefaultPageAndSize() { + // Given + PageResult emptyResult = new PageResult<>(List.of(), 0L); + when(orderQueryService.queryPage(any())).thenReturn(emptyResult); + + // When: 不传分页参数 + controller.queryPage(null, null, null, null, null, null, null, null, null); + + // Then: 应该使用默认值 + verify(orderQueryService, times(1)).queryPage(argThat(query -> + query.getPage() == 1 && query.getSize() == 20 + )); + } +}