feat(ops): update cleaner status and assignment logic

This commit is contained in:
lzh
2026-01-19 14:56:31 +08:00
parent 73bc3b299f
commit b71558ae07
169 changed files with 57972 additions and 162 deletions

View 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.