# IoT Button Event Handlers (Ops Side) Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Implement the missing Ops-side RocketMQ handlers to process "Confirm" and "Query" button events from IoT devices, enabling cleaner interactions (confirming orders and querying status) via their badges. **Architecture:** - **Event-Driven**: Handlers consume `ops.order.confirm` and `ops.order.audit` topics. - **Confirm Flow**: `CleanOrderConfirmEventHandler` receives event -> `OrderLifecycleManager` transitions state -> Send TTS via IoT RPC. - **Query Flow**: `CleanOrderAuditEventHandler` intercepts `IOT_BUTTON_QUERY` -> Queries DB for pending count -> Send TTS via IoT RPC. **Tech Stack:** Java, Spring Boot, RocketMQ, MyBatis Plus, Redis. --- ### Task 1: Create Data Transfer Objects (DTOs) **Files:** - Create: `viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/CleanOrderConfirmEventDTO.java` - Modify: `viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/CleanOrderAuditEventDTO.java` **Step 1: Create CleanOrderConfirmEventDTO** Create the DTO to map the confirm event payload. ```java package com.viewsh.module.ops.environment.integration.dto; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.EqualsAndHashCode; /** * 保洁工单确认事件 DTO *

* 由 IoT 模块发布,Ops 模块消费 * * @author AI */ @Data @EqualsAndHashCode(callSuper = true) public class CleanOrderConfirmEventDTO extends BaseDeviceEventDTO { /** * 工单ID */ @JsonProperty("orderId") private Long orderId; /** * 区域ID */ @JsonProperty("areaId") private Long areaId; /** * 触发来源 */ @JsonProperty("triggerSource") private String triggerSource; /** * 按键ID */ @JsonProperty("buttonId") private Integer buttonId; /** * 设备Key (IoT模块发来的字段名) */ @JsonProperty("deviceKey") private String deviceKey; } ``` **Step 2: Modify CleanOrderAuditEventDTO** Add `triggerSource` to enable filtering of query events. ```java // Add this field to CleanOrderAuditEventDTO class /** * 触发来源 (如 IOT_BUTTON_QUERY) */ private String triggerSource; ``` **Step 3: Verification** Run a compile check (or simple test if possible, but DTOs are POJOs). Since we can't easily run `javac` in isolation without classpath, we'll rely on the next steps to verify integration. **Step 4: Commit** ```bash git add viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/CleanOrderConfirmEventDTO.java git add viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/dto/CleanOrderAuditEventDTO.java git commit -m "feat(ops): add confirm event DTO and update audit DTO" ``` --- ### Task 2: Implement CleanOrderConfirmEventHandler **Files:** - Create: `viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderConfirmEventHandler.java` **Step 1: Write the Handler** Implement the logic to consume `ops.order.confirm`. ```java 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.IotDeviceServiceInvokeReqDTO; import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager; import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest; import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.enums.OperatorTypeEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.environment.integration.dto.CleanOrderConfirmEventDTO; import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext; import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope; import com.viewsh.module.ops.infrastructure.log.enumeration.LogType; import com.viewsh.module.ops.infrastructure.log.publisher.BusinessLogPublisher; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; /** * 保洁工单确认事件消费者 *

* 订阅 IoT 模块发布的保洁工单确认事件 (按键确认) * * @author AI */ @Slf4j @Component @RocketMQMessageListener( topic = "ops.order.confirm", consumerGroup = "ops-clean-order-confirm-group", consumeMode = ConsumeMode.CONCURRENTLY, selectorExpression = "*" ) public class CleanOrderConfirmEventHandler implements RocketMQListener { private static final String DEDUP_KEY_PATTERN = "ops:clean:dedup:confirm:%s"; private static final int DEDUP_TTL_SECONDS = 300; @Resource private ObjectMapper objectMapper; @Resource private StringRedisTemplate stringRedisTemplate; @Resource private OpsOrderMapper opsOrderMapper; @Resource private OrderLifecycleManager orderLifecycleManager; @Resource private BusinessLogPublisher businessLogPublisher; @Resource private IotDeviceControlApi iotDeviceControlApi; @Override public void onMessage(String message) { try { CleanOrderConfirmEventDTO event = objectMapper.readValue(message, CleanOrderConfirmEventDTO.class); String dedupKey = String.format(DEDUP_KEY_PATTERN, event.getEventId()); Boolean firstTime = stringRedisTemplate.opsForValue() .setIfAbsent(dedupKey, "1", DEDUP_TTL_SECONDS, TimeUnit.SECONDS); if (!Boolean.TRUE.equals(firstTime)) { log.debug("[CleanOrderConfirmEventHandler] 重复消息,跳过: eventId={}", event.getEventId()); return; } handleOrderConfirm(event); } catch (Exception e) { log.error("[CleanOrderConfirmEventHandler] 处理失败: message={}", message, e); } } private void handleOrderConfirm(CleanOrderConfirmEventDTO event) { log.info("[CleanOrderConfirmEventHandler] 收到确认事件: orderId={}, deviceId={}", event.getOrderId(), event.getDeviceId()); OpsOrderDO order = opsOrderMapper.selectById(event.getOrderId()); if (order == null) { log.warn("[CleanOrderConfirmEventHandler] 工单不存在: orderId={}", event.getOrderId()); return; } WorkOrderStatusEnum status = WorkOrderStatusEnum.fromStatus(order.getStatus()); // 如果已经是确认或进行中状态,视为重复确认,发送提示即可 if (status == WorkOrderStatusEnum.CONFIRMED || status == WorkOrderStatusEnum.ARRIVED) { sendTts(event.getDeviceId(), "工单已在进行中"); return; } if (!status.canConfirm()) { log.warn("[CleanOrderConfirmEventHandler] 状态不允许确认: orderId={}, status={}", event.getOrderId(), status); sendTts(event.getDeviceId(), "当前状态无法确认工单"); return; } // 执行状态转换 try { Map payload = new HashMap<>(); payload.put("deviceId", event.getDeviceId()); payload.put("triggerSource", event.getTriggerSource()); OrderTransitionRequest request = OrderTransitionRequest.builder() .orderId(event.getOrderId()) .targetStatus(WorkOrderStatusEnum.CONFIRMED) .operatorType(OperatorTypeEnum.SYSTEM) .reason("工牌按键确认") .payload(payload) .build(); orderLifecycleManager.transition(request); // 记录日志 BusinessLogContext logContext = BusinessLogContext.builder() .type(LogType.DEVICE) .scope(LogScope.ORDER) .description("保洁员通过工牌确认工单") .targetId(event.getOrderId()) .targetType("order") .success(true) .build(); logContext.putExtra("deviceId", event.getDeviceId()); businessLogPublisher.publishSuccess(logContext); // 发送成功 TTS // TODO: 获取区域名称 String areaName = "作业区域"; // 暂用占位符,实际可查询 AreaDO sendTts(event.getDeviceId(), "工单已确认,请前往" + areaName + "开始作业"); } catch (Exception e) { log.error("[CleanOrderConfirmEventHandler] 状态转换失败", e); sendTts(event.getDeviceId(), "系统异常,确认失败"); } } private void sendTts(Long deviceId, String text) { try { Map params = new HashMap<>(); params.put("text", text); params.put("volume", 80); IotDeviceServiceInvokeReqDTO req = IotDeviceServiceInvokeReqDTO.builder() .deviceId(deviceId) .identifier("playVoice") .params(params) .timeoutSeconds(10) .build(); iotDeviceControlApi.invokeService(req); } catch (Exception e) { log.error("[CleanOrderConfirmEventHandler] TTS发送失败: deviceId={}", deviceId, e); } } } ``` **Step 2: Verification** We will verify by compilation in the final step of the batch, but assume correct given the context. **Step 3: Commit** ```bash git add viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderConfirmEventHandler.java git commit -m "feat(ops): add CleanOrderConfirmEventHandler" ``` --- ### Task 3: Update CleanOrderAuditEventHandler for Query Logic **Files:** - Modify: `viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderAuditEventHandler.java` **Step 1: Modify handleAuditEvent** Add the check for `IOT_BUTTON_QUERY`. ```java // Inside handleAuditEvent method, at the beginning: if ("IOT_BUTTON_QUERY".equals(event.getTriggerSource())) { handleQueryEvent(event); return; } ``` **Step 2: Implement handleQueryEvent** ```java @Resource private OpsOrderMapper opsOrderMapper; // Need to inject this private void handleQueryEvent(CleanOrderAuditEventDTO event) { log.info("[CleanOrderAuditEventHandler] 处理查询事件: deviceId={}", event.getDeviceId()); Long deviceId = event.getDeviceId(); if (deviceId == null) return; // 1. 获取当前工单信息 (for area name) String areaName = "当前区域"; if (event.getOrderId() != null) { OpsOrderDO order = opsOrderMapper.selectById(event.getOrderId()); if (order != null) { // To get area name, we might need AreaMapper or rely on what's in order if cached. // Ideally query Area Service. For now, simplify or check if description/location is usable. // Or just say "当前工单". if (order.getLocation() != null) areaName = order.getLocation(); } } // 2. 查询待办工单数量 // Count orders where assigneeDeviceId == deviceId AND status in (DISPATCHED, QUEUED, CONFIRMED, ARRIVED, PAUSED) // Wait, spec says "待办". Usually implies DISPATCHED/QUEUED. // Let's count all active orders. Long pendingCount = opsOrderMapper.selectCount(new com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX() .eq(OpsOrderDO::getAssigneeDeviceId, deviceId) .in(OpsOrderDO::getStatus, WorkOrderStatusEnum.QUEUED.getStatus(), WorkOrderStatusEnum.DISPATCHED.getStatus(), WorkOrderStatusEnum.CONFIRMED.getStatus(), WorkOrderStatusEnum.ARRIVED.getStatus(), WorkOrderStatusEnum.PAUSED.getStatus()) ); // 3. 构建 TTS String ttsText = String.format("当前位置:%s。待办工单:%d个", areaName, pendingCount); // 4. 发送 TTS (reuse handleTtsRequest logic or call API directly) // Let's reuse the logic but we need to construct a map or call directly. // Since handleTtsRequest takes event with data map, let's just call sendTts helper (duplicate or refactor). // Refactor: extract sendTts method. sendTts(deviceId, ttsText); } private void sendTts(Long deviceId, String text) { // ... (Same logic as in ConfirmHandler, or extract to common helper if possible) // Implementation details omitted for brevity in plan, but copy implementation. } ``` **Step 3: Refactor CleanOrderAuditEventHandler** Extract `sendTts` to avoid code duplication if `handleTtsRequest` logic is similar. **Step 4: Commit** ```bash git add viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderAuditEventHandler.java git commit -m "feat(ops): support IOT_BUTTON_QUERY in audit handler" ``` --- ### Task 4: Build Verification **Step 1: Build Module** Run Maven build to ensure no compilation errors. ```bash mvn clean compile -pl viewsh-module-ops/viewsh-module-environment-biz -am ``` **Step 2: Commit Fixes** If build fails, fix and commit.