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,258 @@
package com.viewsh.module.ops.core.manual;
import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager;
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest;
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionResult;
import com.viewsh.module.ops.core.manual.audit.OrderAuditService;
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.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 统一手动动作门面
* <p>
* 所有手动动作(派单/升级/取消/完单)的统一执行骨架:
* <ol>
* <li>查询工单并校验业务类型</li>
* <li>校验基础状态</li>
* <li>调用条线策略做前置校验</li>
* <li>执行状态变更(通过 OrderLifecycleManager</li>
* <li>写统一审计记录</li>
* <li>调用条线策略处理后置逻辑</li>
* </ol>
* <p>
* 注意手动创建create因各条线差异过大不在此门面中统一
* 由各条线的专用服务直接实现。
*
* @author lzh
*/
@Slf4j
@Service
public class ManualOrderActionFacade {
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private OrderLifecycleManager orderLifecycleManager;
@Resource
private OrderAuditService orderAuditService;
@Resource
private List<OrderBusinessStrategy> strategies;
// ==================== 手动派单 ====================
@Transactional(rollbackFor = Exception.class)
public void dispatch(DispatchOrderCommand cmd) {
// 1. 查单 + 校验
OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType());
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus());
if (status != WorkOrderStatusEnum.PENDING) {
throw new IllegalStateException("当前工单状态不允许手动派单,仅 PENDING 状态可操作");
}
// 2. 条线前置校验
OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType());
strategy.validateDispatch(cmd, order);
// 3. 状态变更
OrderTransitionRequest request = OrderTransitionRequest.builder()
.orderId(cmd.getOrderId())
.targetStatus(WorkOrderStatusEnum.DISPATCHED)
.assigneeId(cmd.getAssigneeId())
.assigneeName(cmd.getAssigneeName())
.operatorType(cmd.getOperator().getOperatorType())
.operatorId(cmd.getOperator().getOperatorId())
.reason(cmd.getReason() != null ? cmd.getReason() : "管理员手动派单")
.build();
OrderTransitionResult result = orderLifecycleManager.transition(request);
if (!result.isSuccess()) {
throw new IllegalStateException("手动派单失败: " + result.getMessage());
}
// 4. 更新主表执行人
order.setAssigneeId(cmd.getAssigneeId());
order.setAssigneeName(cmd.getAssigneeName());
opsOrderMapper.updateById(order);
// 5. 审计
Map<String, Object> payload = new HashMap<>();
payload.put(OrderAuditPayloadKeys.ASSIGNEE_ID, cmd.getAssigneeId());
payload.put(OrderAuditPayloadKeys.ASSIGNEE_NAME, cmd.getAssigneeName());
if (cmd.getReason() != null) {
payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason());
}
String message = String.format("管理员手动派单给 %s",
cmd.getAssigneeName() != null ? cmd.getAssigneeName() : cmd.getAssigneeId());
orderAuditService.record(order, ManualActionTypeEnum.MANUAL_DISPATCH,
cmd.getOperator(), message, payload);
// 6. 条线后置
strategy.afterDispatch(cmd, order);
log.info("[ManualOrderActionFacade] 手动派单完成: orderId={}, assigneeId={}", cmd.getOrderId(), cmd.getAssigneeId());
}
// ==================== 手动升级优先级 ====================
@Transactional(rollbackFor = Exception.class)
public void upgradePriority(UpgradePriorityCommand cmd) {
// 1. 查单 + 校验
OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType());
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus());
if (status == WorkOrderStatusEnum.COMPLETED || status == WorkOrderStatusEnum.CANCELLED) {
throw new IllegalStateException("已完成或已取消的工单不允许升级优先级");
}
// 幂等
Integer oldPriority = order.getPriority();
cmd.setOldPriority(oldPriority);
if (oldPriority != null && oldPriority.equals(cmd.getNewPriority())) {
log.info("[ManualOrderActionFacade] 优先级未变化,幂等返回: orderId={}", cmd.getOrderId());
return;
}
// 2. 条线前置校验
OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType());
strategy.validateUpgradePriority(cmd, order);
// 3. 更新优先级
order.setPriority(cmd.getNewPriority());
opsOrderMapper.updateById(order);
// 4. 审计
Map<String, Object> payload = new HashMap<>();
payload.put(OrderAuditPayloadKeys.OLD_PRIORITY, oldPriority);
payload.put(OrderAuditPayloadKeys.NEW_PRIORITY, cmd.getNewPriority());
if (cmd.getReason() != null) {
payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason());
}
String message = String.format("升级优先级 P%d → P%d",
oldPriority != null ? oldPriority : 2, cmd.getNewPriority());
orderAuditService.record(order, ManualActionTypeEnum.MANUAL_UPGRADE_PRIORITY,
cmd.getOperator(), message, payload);
// 5. 条线后置
strategy.afterUpgradePriority(cmd, order);
log.info("[ManualOrderActionFacade] 升级优先级完成: orderId={}, {} → {}",
cmd.getOrderId(), oldPriority, cmd.getNewPriority());
}
// ==================== 手动取消 ====================
@Transactional(rollbackFor = Exception.class)
public void cancel(CancelOrderCommand cmd) {
// 1. 查单 + 校验
OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType());
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus());
// 幂等
if (status == WorkOrderStatusEnum.CANCELLED) {
log.info("[ManualOrderActionFacade] 工单已取消,幂等返回: orderId={}", cmd.getOrderId());
return;
}
if (status == WorkOrderStatusEnum.COMPLETED) {
throw new IllegalStateException("已完成的工单不能取消");
}
// 2. 条线前置校验
OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType());
strategy.validateCancel(cmd, order);
// 3. 状态变更
orderLifecycleManager.cancelOrder(cmd.getOrderId(),
cmd.getOperator().getOperatorId(),
cmd.getOperator().getOperatorType(),
cmd.getReason());
// 4. 审计
Map<String, Object> payload = new HashMap<>();
payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason());
orderAuditService.record(order, ManualActionTypeEnum.MANUAL_CANCEL,
cmd.getOperator(), "管理员手动取消: " + cmd.getReason(), payload);
// 5. 条线后置
strategy.afterCancel(cmd, order);
log.info("[ManualOrderActionFacade] 手动取消完成: orderId={}", cmd.getOrderId());
}
// ==================== 手动完单 ====================
@Transactional(rollbackFor = Exception.class)
public void complete(CompleteOrderCommand cmd) {
// 1. 查单 + 校验
OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType());
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus());
// 幂等
if (status == WorkOrderStatusEnum.COMPLETED) {
log.info("[ManualOrderActionFacade] 工单已完成,幂等返回: orderId={}", cmd.getOrderId());
return;
}
if (status == WorkOrderStatusEnum.CANCELLED) {
throw new IllegalStateException("已取消的工单不能完成");
}
// 2. 条线前置校验
OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType());
strategy.validateComplete(cmd, order);
// 3. 状态变更
String remark = cmd.getReason() != null ? cmd.getReason() : "管理员手动完成";
orderLifecycleManager.completeOrder(cmd.getOrderId(),
cmd.getOperator().getOperatorId(),
cmd.getOperator().getOperatorType(),
remark);
// 4. 审计
Map<String, Object> payload = new HashMap<>();
if (cmd.getReason() != null) {
payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason());
}
if (cmd.getResult() != null) {
payload.put(OrderAuditPayloadKeys.RESULT, cmd.getResult());
}
orderAuditService.record(order, ManualActionTypeEnum.MANUAL_COMPLETE,
cmd.getOperator(), remark, payload);
// 5. 条线后置
strategy.afterComplete(cmd, order);
log.info("[ManualOrderActionFacade] 手动完单完成: orderId={}", cmd.getOrderId());
}
// ==================== 内部方法 ====================
private OpsOrderDO getOrderAndValidateType(Long orderId, String expectedType) {
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
throw new IllegalArgumentException("工单不存在: orderId=" + orderId);
}
if (!expectedType.equals(order.getOrderType())) {
throw new IllegalStateException(
String.format("工单类型不匹配: 期望 %s实际 %s", expectedType, order.getOrderType()));
}
return order;
}
private OrderBusinessStrategy resolveStrategy(String businessType) {
return strategies.stream()
.filter(s -> s.supports(businessType))
.findFirst()
.orElseThrow(() -> new IllegalStateException("未找到业务条线策略: " + businessType));
}
}

View File

@@ -0,0 +1,122 @@
package com.viewsh.module.ops.core.manual.audit;
import com.viewsh.module.ops.core.manual.model.OperatorContext;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.enums.ManualActionTypeEnum;
import com.viewsh.module.ops.enums.OrderAuditPayloadKeys;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogModule;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
import com.viewsh.module.ops.service.event.OpsOrderEventService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 统一手动动作审计服务
* <p>
* 所有手动动作通过此服务落库,确保 ops_order_event时间轴
* ops_business_event_log审计检索双写一致。
* <p>
* personId 永远表示真实操作者,被操作目标放入 payload。
*
* @author lzh
*/
@Slf4j
@Service
public class OrderAuditService {
@Resource
private OpsOrderEventService opsOrderEventService;
@Resource
private EventLogRecorder eventLogRecorder;
/**
* 记录手动动作审计(双写:时间轴 + 业务日志)
*
* @param order 工单
* @param actionType 手动动作类型
* @param operator 操作人上下文
* @param message 可读消息
* @param payload 审计扩展数据
*/
public void record(OpsOrderDO order, ManualActionTypeEnum actionType,
OperatorContext operator, String message,
Map<String, Object> payload) {
Long orderId = order.getId();
// 1. 时间轴事件ops_order_event
String eventType = mapToEventType(actionType);
try {
opsOrderEventService.recordEvent(
orderId,
order.getStatus(),
order.getStatus(),
eventType,
operator.getOperatorType().getType(),
operator.getOperatorId(),
message
);
} catch (Exception e) {
log.warn("[OrderAuditService] 记录时间轴事件失败: orderId={}, actionType={}", orderId, actionType, e);
}
// 2. 业务审计日志ops_business_event_log
try {
Map<String, Object> auditPayload = new HashMap<>();
auditPayload.put(OrderAuditPayloadKeys.BUSINESS_TYPE, order.getOrderType());
auditPayload.put(OrderAuditPayloadKeys.ACTION_TYPE, actionType.getType());
auditPayload.put(OrderAuditPayloadKeys.MANUAL, true);
auditPayload.put(OrderAuditPayloadKeys.SOURCE, operator.getSource().getSource());
if (payload != null) {
auditPayload.putAll(payload);
}
String module = LogModule.fromOrderType(order.getOrderType());
eventLogRecorder.record(EventLogRecord.builder()
.module(module)
.domain(EventDomain.AUDIT)
.eventType(mapToLogType(actionType).getCode())
.message(message)
.targetId(orderId)
.targetType("order")
.personId(operator.getOperatorId())
.payload(auditPayload)
.build());
} catch (Exception e) {
log.warn("[OrderAuditService] 记录审计日志失败: orderId={}, actionType={}", orderId, actionType, e);
}
}
/**
* ManualActionTypeEnum → ops_order_event eventType
*/
private String mapToEventType(ManualActionTypeEnum actionType) {
return switch (actionType) {
case MANUAL_CREATE -> "CREATE";
case MANUAL_DISPATCH -> "DISPATCH";
case MANUAL_UPGRADE_PRIORITY -> "UPGRADE_PRIORITY";
case MANUAL_CANCEL -> "CANCEL";
case MANUAL_COMPLETE -> "COMPLETE";
};
}
/**
* ManualActionTypeEnum → LogType
*/
private LogType mapToLogType(ManualActionTypeEnum actionType) {
return switch (actionType) {
case MANUAL_CREATE -> LogType.ORDER_CREATED;
case MANUAL_DISPATCH -> LogType.ORDER_DISPATCHED;
case MANUAL_UPGRADE_PRIORITY -> LogType.PRIORITY_UPGRADE;
case MANUAL_CANCEL -> LogType.ORDER_CANCELLED;
case MANUAL_COMPLETE -> LogType.ORDER_COMPLETED;
};
}
}

View File

@@ -0,0 +1,26 @@
package com.viewsh.module.ops.core.manual.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 手动取消工单命令
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CancelOrderCommand {
private String businessType;
private Long orderId;
private OperatorContext operator;
private String reason;
private Map<String, Object> payload;
}

View File

@@ -0,0 +1,27 @@
package com.viewsh.module.ops.core.manual.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 手动完单命令
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CompleteOrderCommand {
private String businessType;
private Long orderId;
private OperatorContext operator;
private String reason;
private String result;
private Map<String, Object> payload;
}

View File

@@ -0,0 +1,29 @@
package com.viewsh.module.ops.core.manual.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 手动派单命令
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DispatchOrderCommand {
private String businessType;
private Long orderId;
private OperatorContext operator;
private Long assigneeId;
private String assigneeName;
private String assigneePhone;
private String reason;
private Map<String, Object> payload;
}

View File

@@ -0,0 +1,62 @@
package com.viewsh.module.ops.core.manual.model;
import com.viewsh.module.ops.enums.OperatorTypeEnum;
import com.viewsh.module.ops.enums.OrderActionSourceEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 统一操作人上下文
* <p>
* 所有手动动作命令必须携带此上下文,确保审计日志和时间轴事件
* 能稳定识别真实操作者。
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OperatorContext {
/** 操作人ID */
private Long operatorId;
/** 操作人姓名(冗余,可由服务层填充) */
private String operatorName;
/** 操作人角色(管理员、安保人员、系统等角色语义) */
private String operatorRole;
/** 操作人类型(兼容状态机和事件枚举) */
private OperatorTypeEnum operatorType;
/** 动作来源 */
@Builder.Default
private OrderActionSourceEnum source = OrderActionSourceEnum.ADMIN_CONSOLE;
/**
* 便捷工厂:管理后台操作
*/
public static OperatorContext ofAdmin(Long operatorId) {
return OperatorContext.builder()
.operatorId(operatorId)
.operatorType(OperatorTypeEnum.ADMIN)
.source(OrderActionSourceEnum.ADMIN_CONSOLE)
.build();
}
/**
* 便捷工厂:管理后台操作(带姓名)
*/
public static OperatorContext ofAdmin(Long operatorId, String operatorName) {
return OperatorContext.builder()
.operatorId(operatorId)
.operatorName(operatorName)
.operatorType(OperatorTypeEnum.ADMIN)
.source(OrderActionSourceEnum.ADMIN_CONSOLE)
.build();
}
}

View File

@@ -0,0 +1,28 @@
package com.viewsh.module.ops.core.manual.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 手动升级优先级命令
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpgradePriorityCommand {
private String businessType;
private Long orderId;
private OperatorContext operator;
private Integer oldPriority;
private Integer newPriority;
private String reason;
private Map<String, Object> payload;
}

View File

@@ -0,0 +1,42 @@
package com.viewsh.module.ops.core.manual.strategy;
import com.viewsh.module.ops.core.manual.model.*;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
/**
* 工单业务条线策略接口
* <p>
* 每个业务条线(保洁、安保、工程、客服)实现此接口,
* 负责条线特有的前置校验和后置副作用。
* <p>
* 默认方法为空实现,条线按需覆写。
*
* @author lzh
*/
public interface OrderBusinessStrategy {
/**
* 是否支持指定业务类型
*/
boolean supports(String businessType);
// ==================== 前置校验 ====================
default void validateDispatch(DispatchOrderCommand cmd, OpsOrderDO order) {}
default void validateUpgradePriority(UpgradePriorityCommand cmd, OpsOrderDO order) {}
default void validateCancel(CancelOrderCommand cmd, OpsOrderDO order) {}
default void validateComplete(CompleteOrderCommand cmd, OpsOrderDO order) {}
// ==================== 后置处理 ====================
default void afterDispatch(DispatchOrderCommand cmd, OpsOrderDO order) {}
default void afterUpgradePriority(UpgradePriorityCommand cmd, OpsOrderDO order) {}
default void afterCancel(CancelOrderCommand cmd, OpsOrderDO order) {}
default void afterComplete(CompleteOrderCommand cmd, OpsOrderDO order) {}
}