Files
aiot-platform-cloud/docs/plans/2026-01-19-iot-button-handlers-ops.md

396 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
* <p>
* 由 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;
/**
* 保洁工单确认事件消费者
* <p>
* 订阅 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<String> {
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<String, Object> 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<String, Object> 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<OpsOrderDO>()
.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.