feat(clean): 保洁条线接入 ManualOrderActionFacade

CleanOrderBusinessStrategy:
- validateDispatch: 校验设备在线/可接单/区域匹配
- afterUpgradePriority: adjustPriority + rebuildWaitingTasks + P0 通知

CleanWorkOrderServiceImpl:
- manualCreateOrder: 主表+扩展表+创建事件+审计日志
- manualDispatch/manualCancelOrder/manualCompleteOrder/upgradePriority: 委托 facade
- 所有入口统一 resolveUserName 传入 operatorName

UpgradePriorityReqDTO:新增 newPriority/operatorId 字段

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-27 16:08:20 +08:00
parent 333329c29c
commit 5d91097e75
4 changed files with 266 additions and 88 deletions

View File

@@ -1,6 +1,9 @@
package com.viewsh.module.ops.environment.service.cleanorder;
import com.viewsh.module.ops.api.clean.OrderTimelineRespDTO;
import com.viewsh.module.ops.environment.service.cleanorder.dto.CleanManualCancelReqDTO;
import com.viewsh.module.ops.environment.service.cleanorder.dto.CleanManualCreateReqDTO;
import com.viewsh.module.ops.environment.service.cleanorder.dto.CleanManualDispatchReqDTO;
import com.viewsh.module.ops.environment.service.cleanorder.dto.ManualCompleteOrderReqDTO;
import com.viewsh.module.ops.environment.service.cleanorder.dto.UpgradePriorityReqDTO;
@@ -47,4 +50,32 @@ public interface CleanWorkOrderService {
* @param req 升级优先级请求
*/
void upgradePriority(UpgradePriorityReqDTO req);
/**
* 手动创建保洁工单
* <p>
* 管理员手动创建保洁工单,补齐主表 + 扩展表 + 创建事件 + 审计日志
*
* @param req 手动创建请求
* @return 工单ID
*/
Long manualCreateOrder(CleanManualCreateReqDTO req);
/**
* 手动派单
* <p>
* 管理员指定设备/人员,绕过自动调度直接派单
*
* @param req 手动派单请求
*/
void manualDispatch(CleanManualDispatchReqDTO req);
/**
* 手动取消工单
* <p>
* 管理员手动取消工单,走完整生命周期链(停止播报、恢复状态、自动派发下一单)
*
* @param req 手动取消请求
*/
void manualCancelOrder(CleanManualCancelReqDTO req);
}

View File

@@ -2,16 +2,30 @@ package com.viewsh.module.ops.environment.service.cleanorder;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.api.clean.OrderTimelineRespDTO;
import com.viewsh.module.ops.api.queue.OrderQueueService;
import com.viewsh.module.ops.core.event.OrderCreatedEvent;
import com.viewsh.module.ops.core.event.OrderEventPublisher;
import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager;
import com.viewsh.module.ops.core.manual.ManualOrderActionFacade;
import com.viewsh.module.ops.core.manual.audit.OrderAuditService;
import com.viewsh.module.ops.core.manual.model.*;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderEventDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderEventMapper;
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.service.cleanorder.dto.ManualCompleteOrderReqDTO;
import com.viewsh.module.ops.environment.service.cleanorder.dto.UpgradePriorityReqDTO;
import com.viewsh.module.ops.enums.*;
import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO;
import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper;
import com.viewsh.module.ops.environment.service.cleanorder.dto.*;
import com.viewsh.module.ops.infrastructure.area.AreaPathBuilder;
import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
import com.viewsh.module.ops.service.event.OpsOrderEventService;
import com.viewsh.module.system.api.user.AdminUserApi;
import com.viewsh.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -41,10 +55,33 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
private OpsOrderEventMapper opsOrderEventMapper;
@Resource
private OpsOrderEventService opsOrderEventService;
private AdminUserApi adminUserApi;
@Resource
private OrderLifecycleManager orderLifecycleManager;
private OrderIdGenerator orderIdGenerator;
@Resource
private OrderCodeGenerator orderCodeGenerator;
@Resource
private OrderEventPublisher orderEventPublisher;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Resource
private AreaPathBuilder areaPathBuilder;
@Resource
private OpsOrderCleanExtMapper cleanExtMapper;
@Resource
private ManualOrderActionFacade manualOrderActionFacade;
@Resource
private OrderAuditService orderAuditService;
private static final String BUSINESS_TYPE = WorkOrderTypeEnum.CLEAN.getType();
/**
* 事件类型名称映射
@@ -124,75 +161,24 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void manualCompleteOrder(ManualCompleteOrderReqDTO req) {
Long orderId = req.getOrderId();
// 1. 查询工单
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
throw new IllegalArgumentException("工单不存在: orderId=" + orderId);
}
// 2. 校验:已完成的工单幂等返回
if (WorkOrderStatusEnum.COMPLETED.getStatus().equals(order.getStatus())) {
log.info("[manualCompleteOrder] 工单已处于完成状态,幂等返回: orderId={}", orderId);
return;
}
// 3. 校验:已取消的工单不能完成
if (WorkOrderStatusEnum.CANCELLED.getStatus().equals(order.getStatus())) {
throw new IllegalStateException("已取消的工单不能完成: orderId=" + orderId);
}
// 4. 通过 OrderLifecycleManager 走完整责任链(状态校验→队列同步→事件发布)
String remark = req.getRemark() != null ? req.getRemark() : "管理员手动完成";
orderLifecycleManager.completeOrder(orderId, req.getOperatorId(), OperatorTypeEnum.ADMIN, remark);
log.info("[manualCompleteOrder] 手动完成工单成功: orderId={}, operatorId={}, remark={}",
orderId, req.getOperatorId(), remark);
manualOrderActionFacade.complete(CompleteOrderCommand.builder()
.businessType(BUSINESS_TYPE)
.orderId(req.getOrderId())
.operator(OperatorContext.ofAdmin(req.getOperatorId(), resolveUserName(req.getOperatorId())))
.reason(req.getRemark())
.build());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void upgradePriority(UpgradePriorityReqDTO req) {
try {
Long orderId = req.getOrderId();
// 1. 查询工单
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
log.warn("[upgradePriority] 工单不存在: orderId={}", orderId);
return;
}
Integer oldPriority = order.getPriority();
// 2. 更新工单优先级为 P0
OpsOrderDO updateDO = new OpsOrderDO();
updateDO.setId(orderId);
updateDO.setPriority(0); // P0
opsOrderMapper.updateById(updateDO);
// 3. 记录事件
opsOrderEventService.recordEvent(
orderId,
order.getStatus(),
order.getStatus(),
"UPGRADE_PRIORITY",
"ADMIN",
null, // 管理员操作
String.format("优先级从 P%d 升级为 P0: %s",
oldPriority != null ? oldPriority : 1, req.getReason())
);
log.info("[upgradePriority] 升级工单优先级成功: orderId={}, oldPriority={}, reason={}",
orderId, oldPriority, req.getReason());
} catch (Exception e) {
log.error("[upgradePriority] 升级工单优先级失败: orderId={}", req.getOrderId(), e);
throw e;
}
manualOrderActionFacade.upgradePriority(UpgradePriorityCommand.builder()
.businessType(BUSINESS_TYPE)
.orderId(req.getOrderId())
.operator(OperatorContext.ofAdmin(req.getOperatorId(), resolveUserName(req.getOperatorId())))
.newPriority(req.getEffectiveNewPriority())
.reason(req.getReason())
.build());
}
// ==================== 私有方法 ====================
@@ -208,7 +194,7 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
String operator = event.getOperatorName();
if (operator == null || operator.isEmpty()) {
if ("SYSTEM".equals(event.getOperatorType())) {
if (OperatorTypeEnum.SYSTEM.getType().equals(event.getOperatorType())) {
operator = "系统";
} else {
operator = "操作员";
@@ -259,8 +245,8 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
*/
private Map<String, Object> buildPendingExtraInfo(OpsOrderDO order) {
Map<String, Object> extra = new HashMap<>();
extra.put("event_type", "CREATE");
extra.put("operator_type", "SYSTEM");
extra.put("event_type", OrderEventTypeEnum.CREATE.getType());
extra.put("operator_type", OperatorTypeEnum.SYSTEM.getType());
extra.put("order_code", order.getOrderCode());
extra.put("priority", order.getPriority());
extra.put("source_type", order.getSourceType());
@@ -269,4 +255,111 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
}
return extra;
}
/**
* 根据用户 ID 查询姓名,查询失败返回 null不影响主流程
*/
private String resolveUserName(Long userId) {
if (userId == null) {
return null;
}
try {
AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData();
return user != null ? user.getNickname() : null;
} catch (Exception e) {
log.debug("[resolveUserName] 查询用户姓名失败: userId={}", userId);
return null;
}
}
// ==================== 手动创建 ====================
@Override
@Transactional(rollbackFor = Exception.class)
public Long manualCreateOrder(CleanManualCreateReqDTO req) {
// 0. 校验区域
OpsBusAreaDO area = opsBusAreaMapper.selectById(req.getAreaId());
if (area == null) {
throw new IllegalArgumentException("区域不存在: areaId=" + req.getAreaId());
}
// 1. 生成ID和编号
Long orderId = orderIdGenerator.generate();
String orderCode = orderCodeGenerator.generate(BUSINESS_TYPE);
// 2. 插入主表
OpsOrderDO order = OpsOrderDO.builder()
.id(orderId)
.orderCode(orderCode)
.orderType(BUSINESS_TYPE)
.sourceType(SourceTypeEnum.MANUAL.getType())
.title(req.getTitle())
.description(req.getDescription())
.priority(req.getPriority() != null ? req.getPriority() : PriorityEnum.P2.getPriority())
.status(WorkOrderStatusEnum.PENDING.getStatus())
.areaId(req.getAreaId())
.location(areaPathBuilder.buildPath(area))
.build();
opsOrderMapper.insert(order);
// 3. 插入扩展表
OpsOrderCleanExtDO cleanExt = OpsOrderCleanExtDO.builder()
.id(orderIdGenerator.generate())
.opsOrderId(orderId)
.isAuto(0) // 手动创建
.expectedDuration(req.getExpectedDuration())
.cleaningType(req.getCleaningType())
.build();
cleanExtMapper.insert(cleanExt);
// 4. 发布工单创建事件(触发自动派单)
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(orderId)
.orderType(BUSINESS_TYPE)
.orderCode(orderCode)
.title(req.getTitle())
.areaId(req.getAreaId())
.priority(order.getPriority())
.createTime(order.getCreateTime())
.build()
.addPayload("isAuto", false);
orderEventPublisher.publishOrderCreated(event);
// 5. 记录审计日志ops_business_event_log
String operatorName = resolveUserName(req.getOperatorId());
String message = operatorName != null
? String.format("%s 创建了保洁工单", operatorName)
: "手动创建保洁工单";
orderAuditService.record(order, ManualActionTypeEnum.MANUAL_CREATE,
OperatorContext.ofAdmin(req.getOperatorId(), operatorName), message, null);
log.info("[manualCreateOrder] 手动创建保洁工单成功: orderId={}, orderCode={}, operatorId={}",
orderId, orderCode, req.getOperatorId());
return orderId;
}
// ==================== 手动派单(委托 Facade ====================
@Override
public void manualDispatch(CleanManualDispatchReqDTO req) {
manualOrderActionFacade.dispatch(DispatchOrderCommand.builder()
.businessType(BUSINESS_TYPE)
.orderId(req.getOrderId())
.operator(OperatorContext.ofAdmin(req.getOperatorId(), resolveUserName(req.getOperatorId())))
.assigneeId(req.getAssigneeId())
.reason(req.getRemark())
.build());
}
// ==================== 手动取消(委托 Facade ====================
@Override
public void manualCancelOrder(CleanManualCancelReqDTO req) {
manualOrderActionFacade.cancel(CancelOrderCommand.builder()
.businessType(BUSINESS_TYPE)
.orderId(req.getOrderId())
.operator(OperatorContext.ofAdmin(req.getOperatorId(), resolveUserName(req.getOperatorId())))
.reason(req.getReason())
.build());
}
}

View File

@@ -7,8 +7,6 @@ import lombok.Data;
/**
* 升级工单优先级请求 DTO
* <p>
* 用于将普通工单升级为 P0 紧急工单
*
* @author lzh
*/
@@ -20,7 +18,20 @@ public class UpgradePriorityReqDTO {
@NotNull(message = "工单ID不能为空")
private Long orderId;
@Schema(description = "目标优先级0=P0紧急, 1=P1重要, 2=P2普通默认 0P0", example = "0")
private Integer newPriority;
@Schema(description = "升级原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户投诉急需处理")
@NotBlank(message = "升级原因不能为空")
private String reason;
@Schema(description = "操作人ID由 Controller 注入)", hidden = true)
private Long operatorId;
/**
* 获取目标优先级,默认 P0
*/
public Integer getEffectiveNewPriority() {
return newPriority != null ? newPriority : 0;
}
}

View File

@@ -1,21 +1,22 @@
package com.viewsh.module.ops.environment.service.manual;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.api.queue.OrderQueueDTO;
import com.viewsh.module.ops.api.queue.OrderQueueService;
import com.viewsh.module.ops.core.manual.model.*;
import com.viewsh.module.ops.core.manual.model.DispatchOrderCommand;
import com.viewsh.module.ops.core.manual.model.UpgradePriorityCommand;
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.PriorityEnum;
import com.viewsh.module.ops.enums.WorkOrderTypeEnum;
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
import com.viewsh.module.ops.environment.service.notification.CleanOrderNotificationService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 保洁条线策略
* <p>
* 处理保洁特有的前置校验和后置副作用。
*
* @author lzh
* 保洁条线策略
*/
@Slf4j
@Component
@@ -24,18 +25,60 @@ public class CleanOrderBusinessStrategy implements OrderBusinessStrategy {
@Resource
private OrderQueueService orderQueueService;
@Resource
private CleanOrderNotificationService cleanOrderNotificationService;
@Resource
private BadgeDeviceStatusService badgeDeviceStatusService;
@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());
public void validateDispatch(DispatchOrderCommand cmd, OpsOrderDO order) {
if (cmd.getAssigneeId() == null) {
throw new IllegalArgumentException("手动派单目标设备不能为空");
}
BadgeDeviceStatusDTO badge = badgeDeviceStatusService.getBadgeStatus(cmd.getAssigneeId());
if (badge == null) {
throw new IllegalStateException("目标保洁设备不存在");
}
if (!badge.isOnline()) {
throw new IllegalStateException("目标保洁设备当前离线,不能手动派单");
}
if (!badge.canAcceptNewOrder()) {
throw new IllegalStateException("目标保洁设备当前不可接单");
}
if (order.getAreaId() != null && badge.getCurrentAreaId() == null) {
throw new IllegalStateException("目标保洁设备当前未绑定区域,不能手动派单");
}
if (order.getAreaId() != null && !order.getAreaId().equals(badge.getCurrentAreaId())) {
throw new IllegalStateException("目标保洁设备不在当前工单所属区域");
}
}
@Override
public void afterUpgradePriority(UpgradePriorityCommand cmd, OpsOrderDO order) {
OrderQueueDTO queueDTO = orderQueueService.getByOpsOrderId(cmd.getOrderId());
if (queueDTO == null) {
return;
}
PriorityEnum newPriority = PriorityEnum.fromPriority(cmd.getNewPriority());
String reason = cmd.getReason() != null ? cmd.getReason() : "手动升级优先级";
orderQueueService.adjustPriority(queueDTO.getId(), newPriority, reason);
orderQueueService.rebuildWaitingTasksByUserId(queueDTO.getUserId(), order.getAreaId());
if (newPriority == PriorityEnum.P0) {
cleanOrderNotificationService.sendPriorityUpgradeNotification(
queueDTO.getUserId(), order.getOrderCode(), cmd.getOrderId());
}
log.info("[CleanStrategy] 升级优先级后置完成: orderId={}, newPriority={}, queueId={}",
cmd.getOrderId(), newPriority, queueDTO.getId());
}
}