feat(ops): update cleaner status and assignment logic
This commit is contained in:
395
docs/plans/2026-01-19-iot-button-handlers-ops.md
Normal file
395
docs/plans/2026-01-19-iot-button-handlers-ops.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# 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.
|
||||
|
||||
Reference in New Issue
Block a user