feat(ops): add confirm event DTO and update audit DTO

This commit is contained in:
lzh
2026-01-19 14:41:05 +08:00
parent a71a29f548
commit eedef70581
7 changed files with 889 additions and 2 deletions

View File

@@ -0,0 +1,140 @@
package com.viewsh.module.ops.environment.integration.adapter;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.core.dispatch.model.AssigneeStatus;
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
import java.time.LocalDateTime;
/**
* 工牌设备状态适配器
* <p>
* 将 {@link BadgeDeviceStatusDTO} 适配为通用的 {@link AssigneeStatus} 接口
* <p>
* 设计说明:
* - 适配器模式:将工牌设备状态对象转换为通用接口
* - 设备即执行人用工牌设备ID作为执行人ID参与调度
* - 解耦设计:调度引擎通过通用接口访问设备状态,不依赖具体实现
*
* @author lzh
*/
public class BadgeAssigneeStatusAdapter implements AssigneeStatus {
private final BadgeDeviceStatusDTO badgeStatus;
private final Long waitingTaskCount;
public BadgeAssigneeStatusAdapter(BadgeDeviceStatusDTO badgeStatus) {
this(badgeStatus, 0L);
}
public BadgeAssigneeStatusAdapter(BadgeDeviceStatusDTO badgeStatus, Long waitingTaskCount) {
this.badgeStatus = badgeStatus;
this.waitingTaskCount = waitingTaskCount != null ? waitingTaskCount : 0L;
}
@Override
public String getStatus() {
return badgeStatus != null && badgeStatus.getStatus() != null
? badgeStatus.getStatus().getCode()
: "offline";
}
@Override
public boolean isIdle() {
return badgeStatus != null && badgeStatus.getStatus() == BadgeDeviceStatusEnum.IDLE;
}
@Override
public boolean isBusy() {
return badgeStatus != null && badgeStatus.getStatus() == BadgeDeviceStatusEnum.BUSY;
}
@Override
public boolean isOnline() {
return badgeStatus != null && badgeStatus.isOnline();
}
@Override
public Long getCurrentTaskCount() {
// 有正在执行的工单则返回1否则返回0
return badgeStatus != null && badgeStatus.hasCurrentOrder() ? 1L : 0L;
}
@Override
public Long getWaitingTaskCount() {
return waitingTaskCount;
}
@Override
public Long getAssigneeId() {
// 工牌设备ID作为执行人ID
return badgeStatus != null ? badgeStatus.getDeviceId() : null;
}
@Override
public String getAssigneeName() {
// 工牌设备编码作为执行人名称
return badgeStatus != null ? badgeStatus.getDeviceCode() : null;
}
@Override
public Long getAreaId() {
return badgeStatus != null ? badgeStatus.getCurrentAreaId() : null;
}
@Override
public LocalDateTime getLastHeartbeatTime() {
if (badgeStatus == null || badgeStatus.getLastHeartbeatTime() == null) {
return null;
}
// 将时间戳转换为 LocalDateTime
return LocalDateTime.ofInstant(
java.time.Instant.ofEpochMilli(badgeStatus.getLastHeartbeatTime()),
java.time.ZoneId.systemDefault()
);
}
@Override
public Integer getBatteryLevel() {
return badgeStatus != null ? badgeStatus.getBatteryLevel() : null;
}
@Override
public Object getExtension(String key) {
if (badgeStatus == null) {
return null;
}
// 支持业务特定的扩展属性
switch (key) {
case "currentAreaName":
return badgeStatus.getCurrentAreaName();
case "currentOrderId":
return badgeStatus.getCurrentOpsOrderId();
case "statusChangeTime":
return badgeStatus.getStatusChangeTime();
default:
return null;
}
}
/**
* 获取原始的工牌设备状态对象
*/
public BadgeDeviceStatusDTO getBadgeStatus() {
return badgeStatus;
}
/**
* 创建适配器(静态工厂方法)
*/
public static BadgeAssigneeStatusAdapter of(BadgeDeviceStatusDTO badgeStatus) {
return new BadgeAssigneeStatusAdapter(badgeStatus);
}
/**
* 创建带等待任务数的适配器
*/
public static BadgeAssigneeStatusAdapter of(BadgeDeviceStatusDTO badgeStatus, Long waitingTaskCount) {
return new BadgeAssigneeStatusAdapter(badgeStatus, waitingTaskCount);
}
}

View File

@@ -31,8 +31,13 @@ public class CleanOrderAuditEventDTO {
*/
private Long orderId;
/**
* 审计类型
/**
* 触发来源 (如 IOT_BUTTON_QUERY)
*/
private String triggerSource;
/**
* 审计类型
* <p>
* - BEACON_ARRIVE_CONFIRMED: 蓝牙信标到岗确认
* - BEACON_LEAVE_WARNING_SENT: 离开区域警告已发送

View File

@@ -0,0 +1,47 @@
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;
}

View File

@@ -0,0 +1,204 @@
package com.viewsh.module.ops.environment.integration.handler;
import com.viewsh.module.ops.core.badge.BadgeDeviceStatusService;
import com.viewsh.module.ops.core.event.OrderStateChangedEvent;
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
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;
/**
* 工牌设备状态事件处理器
* <p>
* 监听工单状态变更事件,同步更新工牌设备状态
* <p>
* 设计说明:
* - 使用 Spring EventListener 监听工单状态变更事件
* - 根据工单状态转换规则更新工牌设备状态
* - 支持工单分配、开始、暂停、完成等场景
*
* @author lzh
*/
@Slf4j
@Component
public class BadgeDeviceStatusEventHandler {
@Resource
private BadgeDeviceStatusService badgeDeviceStatusService;
/**
* 处理工单状态变更事件
* <p>
* 状态映射规则:
* - PENDING → ASSIGNED: 不影响设备状态(设备可能还在处理其他任务)
* - ASSIGNED → ARRIVED: 设备状态 → BUSY开始执行任务
* - ARRIVED → PAUSED: 设备状态 → PAUSED任务暂停
* - PAUSED → ARRIVED/BUSY: 设备状态 → BUSY任务恢复
* - ARRIVED → COMPLETED: 设备状态 → IDLE任务完成检查是否还有等待任务
* - 任意状态 → CANCELLED: 清除设备当前工单,检查是否还有等待任务
*
* @param event 工单状态变更事件
*/
@EventListener
@Async("opsExecutor")
public void onOrderStateChanged(OrderStateChangedEvent event) {
try {
// 只处理保洁类型的工单
if (!"CLEAN".equals(event.getOrderType())) {
return;
}
log.debug("[BadgeDeviceStatusEventHandler] 收到工单状态变更事件: orderId={}, {} → {}, operatorId={}",
event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId());
// 获取设备ID从 payload 或 operatorId 获取)
Long deviceId = getDeviceIdFromEvent(event);
if (deviceId == null) {
log.debug("[BadgeDeviceStatusEventHandler] 无法获取设备ID跳过处理: orderId={}", event.getOrderId());
return;
}
// 根据状态转换更新设备状态
handleStatusTransition(deviceId, event);
} catch (Exception e) {
log.error("[BadgeDeviceStatusEventHandler] 处理工单状态变更事件失败: orderId={}", event.getOrderId(), e);
}
}
/**
* 根据状态转换更新设备状态
*/
private void handleStatusTransition(Long deviceId, OrderStateChangedEvent event) {
WorkOrderStatusEnum newStatus = event.getNewStatus();
switch (newStatus) {
case ASSIGNED:
// 工单已分配,不影响设备状态(设备可能还在处理其他任务)
// 设置设备当前工单
badgeDeviceStatusService.setCurrentOrder(deviceId, event.getOrderId());
break;
case ARRIVED:
// 保洁员已到岗,开始执行任务
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY,
event.getOperatorId(), "工单开始执行: " + event.getOrderCode());
badgeDeviceStatusService.setCurrentOrder(deviceId, event.getOrderId());
break;
case PAUSED:
// 任务暂停
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.PAUSED,
event.getOperatorId(), event.getRemark());
break;
case COMPLETED:
// 任务完成
badgeDeviceStatusService.clearCurrentOrder(deviceId);
// 检查是否还有等待任务,如果有则保持 BUSY否则转为 IDLE
checkAndUpdateDeviceStatusAfterCompletion(deviceId, event);
break;
case CANCELLED:
// 工单取消
badgeDeviceStatusService.clearCurrentOrder(deviceId);
// 检查是否还有等待任务,如果有则保持 BUSY否则转为 IDLE
checkAndUpdateDeviceStatusAfterCompletion(deviceId, event);
break;
default:
log.debug("[BadgeDeviceStatusEventHandler] 不处理的状态: {}", newStatus);
break;
}
}
/**
* 任务完成或取消后,检查并更新设备状态
* <p>
* 如果有等待任务,保持 BUSY 状态
* 如果没有等待任务,转为 IDLE 状态
*/
private void checkAndUpdateDeviceStatusAfterCompletion(Long deviceId, OrderStateChangedEvent event) {
// 检查是否还有等待任务(通过查询队列)
// 这里简化处理,直接转为 IDLE
// 实际业务中可能需要查询队列判断是否还有等待任务
// 检查是否是被P0打断的任务恢复
boolean isResumeFromInterrupt = event.hasPayload("interruptReason")
&& "RESUME_FROM_INTERRUPT".equals(event.getPayloadString("interruptReason"));
if (isResumeFromInterrupt) {
// 恢复被中断的任务,保持 BUSY
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY,
event.getOperatorId(), "恢复被中断的任务");
} else {
// 正常完成,转为 IDLE
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.IDLE,
event.getOperatorId(), "任务完成,转为空闲");
}
}
/**
* 从事件中获取设备ID
* <p>
* 优先级:
* 1. payload.deviceId
* 2. operatorId如果 operatorType 是 DEVICE
* 3. 通过工单查询(需要额外的查询,暂不实现)
*/
private Long getDeviceIdFromEvent(OrderStateChangedEvent event) {
// 优先从 payload 获取
Long deviceId = event.getPayloadLong("deviceId");
if (deviceId != null) {
return deviceId;
}
// TODO: 可以通过工单ID查询获取设备ID
// 这需要注入 OrderService 或 OrderMapper
return null;
}
/**
* 处理 P0 紧急任务打断事件
* <p>
* 当 P0 任务需要打断当前任务时:
* 1. 将被打断的设备状态转为 PAUSED
* 2. 记录被打断的工单ID
*
* @param deviceId 被打断的设备ID
* @param interruptedOrderId 被打断的工单ID
* @param urgentOrderId P0紧急工单ID
*/
public void handleP0Interrupt(Long deviceId, Long interruptedOrderId, Long urgentOrderId) {
log.info("[BadgeDeviceStatusEventHandler] P0紧急任务打断: deviceId={}, interruptedOrderId={}, urgentOrderId={}",
deviceId, interruptedOrderId, urgentOrderId);
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.PAUSED,
null, "被P0紧急任务打断: " + urgentOrderId);
}
/**
* 处理 P0 任务完成后的恢复事件
* <p>
* 当 P0 任务完成后,恢复被打断的任务:
* 1. 将设备状态转为 BUSY
* 2. 设置当前工单为被打断的工单
*
* @param deviceId 设备ID
* @param interruptedOrderId 被打断的工单ID
*/
public void handleP0Resume(Long deviceId, Long interruptedOrderId) {
log.info("[BadgeDeviceStatusEventHandler] P0任务完成恢复被中断任务: deviceId={}, interruptedOrderId={}",
deviceId, interruptedOrderId);
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY,
null, "P0任务完成恢复被中断的任务");
badgeDeviceStatusService.setCurrentOrder(deviceId, interruptedOrderId);
}
}