feat(ops): 新增手动操作枚举与模型定义

引入统一手动动作基础设施:
- ManualActionTypeEnum: 手动动作类型(创建/派单/取消/完单/升级)
- OrderActionSourceEnum: 动作来源(管理后台/开放接口)
- OrderAuditPayloadKeys: 审计 payload 标准化 key
- OrderEventTypeEnum: 事件类型枚举值对齐状态机(DISPATCHED/QUEUED/CONFIRMED)
- OperatorContext: 统一操作人上下文
- *Command: 手动动作命令模型(Dispatch/Cancel/Complete/UpgradePriority)
- OrderBusinessStrategy: 条线策略接口

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-27 15:53:20 +08:00
parent 4dffd21751
commit e1d967a65e
21 changed files with 1176 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
package com.viewsh.module.ops.environment.service.cleanorder.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 管理后台 - 保洁手动取消工单 Request DTO
*
* @author lzh
*/
@Schema(description = "管理后台 - 保洁手动取消工单 Request DTO")
@Data
public class CleanManualCancelReqDTO {
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "工单ID不能为空")
private Long orderId;
@Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "误报,无需保洁")
@NotBlank(message = "取消原因不能为空")
private String reason;
@Schema(description = "操作人ID由 Controller 注入)", hidden = true)
private Long operatorId;
}

View File

@@ -0,0 +1,39 @@
package com.viewsh.module.ops.environment.service.cleanorder.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 管理后台 - 保洁手动创建工单 Request DTO
*
* @author lzh
*/
@Schema(description = "管理后台 - 保洁手动创建工单 Request DTO")
@Data
public class CleanManualCreateReqDTO {
@Schema(description = "工单标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "三楼电梯厅紧急清洁")
@NotBlank(message = "工单标题不能为空")
private String title;
@Schema(description = "工单描述", example = "地面有大面积水渍")
private String description;
@Schema(description = "优先级0=P0紧急, 1=P1重要, 2=P2普通", example = "1")
private Integer priority;
@Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "区域ID不能为空")
private Long areaId;
@Schema(description = "保洁类型", example = "SPOT")
private String cleaningType;
@Schema(description = "预计作业时长(分钟)", example = "30")
private Integer expectedDuration;
@Schema(description = "操作人ID由 Controller 注入)", hidden = true)
private Long operatorId;
}

View File

@@ -0,0 +1,29 @@
package com.viewsh.module.ops.environment.service.cleanorder.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 管理后台 - 保洁手动派单 Request DTO
*
* @author lzh
*/
@Schema(description = "管理后台 - 保洁手动派单 Request DTO")
@Data
public class CleanManualDispatchReqDTO {
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "工单ID不能为空")
private Long orderId;
@Schema(description = "目标设备ID工牌设备ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "目标设备ID不能为空")
private Long assigneeId;
@Schema(description = "派单备注", example = "紧急情况,指定该设备处理")
private String remark;
@Schema(description = "操作人ID由 Controller 注入)", hidden = true)
private Long operatorId;
}

View File

@@ -0,0 +1,41 @@
package com.viewsh.module.ops.environment.service.manual;
import com.viewsh.module.ops.api.queue.OrderQueueService;
import com.viewsh.module.ops.core.manual.model.*;
import com.viewsh.module.ops.core.manual.strategy.OrderBusinessStrategy;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.enums.WorkOrderTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 保洁条线策略
* <p>
* 处理保洁特有的前置校验和后置副作用。
*
* @author lzh
*/
@Slf4j
@Component
public class CleanOrderBusinessStrategy implements OrderBusinessStrategy {
@Resource
private OrderQueueService orderQueueService;
@Override
public boolean supports(String businessType) {
return WorkOrderTypeEnum.CLEAN.getType().equals(businessType);
}
@Override
public void afterUpgradePriority(UpgradePriorityCommand cmd, OpsOrderDO order) {
// 如果工单在队列中,触发队列分数重算
if (WorkOrderStatusEnum.QUEUED.getStatus().equals(order.getStatus()) && order.getAssigneeId() != null) {
orderQueueService.rebuildWaitingTasksByUserId(order.getAssigneeId(), order.getAreaId());
log.info("[CleanStrategy] 升级优先级后重算队列: orderId={}, assigneeId={}",
cmd.getOrderId(), order.getAssigneeId());
}
}
}

View File

@@ -0,0 +1,190 @@
package com.viewsh.module.ops.environment.service.notification;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.environment.constants.CleanNotificationConstants;
import com.viewsh.module.ops.environment.service.voice.TtsQueueMessage;
import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService;
import com.viewsh.module.system.api.notify.NotifyMessageSendApi;
import com.viewsh.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 保洁工单通知服务。
* <p>
* 负责语音播报和站内信发送,供事件监听器、业务服务、策略层复用,
* 避免业务层反向依赖事件监听器形成循环依赖。
*/
@Slf4j
@Service
public class CleanOrderNotificationService {
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private VoiceBroadcastService voiceBroadcastService;
@Resource
private NotifyMessageSendApi notifyMessageSendApi;
@Async("ops-task-executor")
public void sendNewOrderNotification(Long deviceId, Long orderId) {
try {
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
log.warn("[新工单通知] 工单不存在: orderId={}", orderId);
return;
}
log.info("[新工单通知] deviceId={}, orderId={}", deviceId, orderId);
voiceBroadcastService.broadcastLoop(deviceId,
CleanNotificationConstants.VoiceTemplate.NEW_ORDER_RING, orderId);
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.NEW_ORDER,
CleanNotificationConstants.NotifyParamsBuilder.newOrderParams(
order.getOrderCode(),
order.getTitle(),
CleanNotificationConstants.VoiceBuilder.getAreaName(order.getLocation())));
} catch (Exception e) {
log.error("[新工单通知] 发送失败: deviceId={}, orderId={}", deviceId, orderId, e);
}
}
@Async("ops-task-executor")
public void sendQueuedOrderNotification(Long deviceId, int queueCount, Long orderId) {
try {
log.info("[待办增加通知] deviceId={}, queueCount={}", deviceId, queueCount);
String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildQueuedOrder(queueCount);
playVoice(deviceId, voiceMessage, orderId);
if (queueCount >= 3) {
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.QUEUED_ORDER,
CleanNotificationConstants.NotifyParamsBuilder.queuedOrderParams(queueCount, 1));
}
} catch (Exception e) {
log.error("[待办增加通知] 发送失败: deviceId={}", deviceId, e);
}
}
@Async("ops-task-executor")
public void sendNextTaskNotification(Long deviceId, int queueCount, String orderTitle) {
try {
log.info("[下一任务通知] deviceId={}, queueCount={}, title={}", deviceId, queueCount, orderTitle);
String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildNextTask(orderTitle);
playVoice(deviceId, voiceMessage);
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.NEXT_TASK,
CleanNotificationConstants.NotifyParamsBuilder.nextTaskParams(queueCount, orderTitle));
} catch (Exception e) {
log.error("[下一任务通知] 发送失败: deviceId={}", deviceId, e);
}
}
@Async("ops-task-executor")
public void sendPriorityUpgradeNotification(Long deviceId, String orderCode, Long orderId) {
try {
log.warn("[P0紧急通知] deviceId={}, orderCode={}", deviceId, orderCode);
String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildPriorityUpgrade(orderCode);
playVoiceUrgent(deviceId, voiceMessage, orderId);
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.PRIORITY_UPGRADE,
CleanNotificationConstants.NotifyParamsBuilder.priorityUpgradeParams(orderCode, "P0紧急任务"));
} catch (Exception e) {
log.error("[P0紧急通知] 发送失败: deviceId={}", deviceId, e);
}
}
@Async("ops-task-executor")
public void sendTaskResumedNotification(Long deviceId, String areaName) {
try {
log.info("[任务恢复通知] deviceId={}, areaName={}", deviceId, areaName);
String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildTaskResumed(areaName);
playVoice(deviceId, voiceMessage);
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.TASK_RESUMED,
CleanNotificationConstants.NotifyParamsBuilder.taskResumedParams(areaName));
} catch (Exception e) {
log.error("[任务恢复通知] 发送失败: deviceId={}", deviceId, e);
}
}
public void sendOrderCompletedNotification(Long orderId, Long deviceId) {
try {
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
return;
}
log.info("[工单完成通知] orderId={}, orderCode={}, areaId={}, deviceId={}",
orderId, order.getOrderCode(), order.getAreaId(), deviceId);
if (deviceId != null) {
playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.ORDER_COMPLETED, orderId);
}
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.ORDER_COMPLETED,
CleanNotificationConstants.NotifyParamsBuilder.orderCompletedParams(
order.getOrderCode(),
CleanNotificationConstants.VoiceBuilder.getAreaName(order.getLocation()),
order.getTitle()));
} catch (Exception e) {
log.error("[工单完成通知] 发送失败: orderId={}, deviceId={}", orderId, deviceId, e);
}
}
@Async("ops-task-executor")
public void playVoiceForNewOrder(Long deviceId) {
playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.NEW_ORDER_SHORT);
}
private void playVoice(Long deviceId, String message) {
playVoice(deviceId, message, null);
}
private void playVoice(Long deviceId, String message, Long orderId) {
try {
voiceBroadcastService.broadcastInOrder(deviceId, message, orderId);
log.debug("[语音播报] 调用成功: deviceId={}, message={}", deviceId, message);
} catch (Exception e) {
log.error("[语音播报] 调用失败: deviceId={}, message={}", deviceId, message, e);
}
}
private void playVoiceUrgent(Long deviceId, String message, Long orderId) {
try {
voiceBroadcastService.broadcastUrgent(deviceId, message, orderId);
log.debug("[语音播报-紧急] 调用成功: deviceId={}, message={}", deviceId, message);
} catch (Exception e) {
log.error("[语音播报-紧急] 调用失败: deviceId={}, message={}", deviceId, message, e);
}
}
private void sendNotifyMessage(Long userId, String templateCode, Map<String, Object> templateParams) {
try {
NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO();
reqDTO.setUserId(userId);
reqDTO.setTemplateCode(templateCode);
reqDTO.setTemplateParams(templateParams);
notifyMessageSendApi.sendSingleMessageToMember(reqDTO);
log.debug("[站内信发送成功] userId={}, templateCode={}", userId, templateCode);
} catch (Exception e) {
log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e);
}
}
}