feat(ops): 新增统一手动动作门面与审计服务

ManualOrderActionFacade:
- 统一骨架:查单校验 → 条线前置 → 状态变更 → 条线后置
- dispatch 支持 PENDING/QUEUED 状态,用独立对象更新 assignee 避免覆盖状态机已写入的 status
- cancel/complete 走 transition() 透传 operatorName 到领域事件
- dispatch/cancel/complete 业务日志由 listener 统一记录,不重复写

OrderAuditService:
- 仅用于 create/upgradePriority(不经过状态机的手动动作)
- 只写 ops_business_event_log,不写 ops_order_event(时间轴只由状态机写)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-27 16:07:13 +08:00
parent e1d967a65e
commit 6b01c29cb1
2 changed files with 85 additions and 135 deletions

View File

@@ -58,9 +58,10 @@ public class ManualOrderActionFacade {
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 状态可操作");
String beforeStatus = order.getStatus();
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(beforeStatus);
if (status != WorkOrderStatusEnum.PENDING && status != WorkOrderStatusEnum.QUEUED) {
throw new IllegalStateException("当前工单状态不允许手动派单,仅 PENDING/QUEUED 状态可操作");
}
// 2. 条线前置校验
@@ -73,33 +74,26 @@ public class ManualOrderActionFacade {
.targetStatus(WorkOrderStatusEnum.DISPATCHED)
.assigneeId(cmd.getAssigneeId())
.assigneeName(cmd.getAssigneeName())
.assigneePhone(cmd.getAssigneePhone())
.operatorType(cmd.getOperator().getOperatorType())
.operatorId(cmd.getOperator().getOperatorId())
.reason(cmd.getReason() != null ? cmd.getReason() : "管理员手动派单")
.operatorName(cmd.getOperator().getOperatorName())
.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);
// 4. 更新主表执行人(只更新 assignee 字段,避免覆盖状态机已写入的 status
OpsOrderDO assigneeUpdate = new OpsOrderDO();
assigneeUpdate.setId(cmd.getOrderId());
assigneeUpdate.setAssigneeId(cmd.getAssigneeId());
assigneeUpdate.setAssigneeName(cmd.getAssigneeName());
opsOrderMapper.updateById(assigneeUpdate);
// 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. 条线后置
// 5. 条线后置
// 注:业务日志由生命周期事件 → 条线 EventListener 统一记录,此处不重复写
strategy.afterDispatch(cmd, order);
log.info("[ManualOrderActionFacade] 手动派单完成: orderId={}, assigneeId={}", cmd.getOrderId(), cmd.getAssigneeId());
@@ -111,7 +105,8 @@ public class ManualOrderActionFacade {
public void upgradePriority(UpgradePriorityCommand cmd) {
// 1. 查单 + 校验
OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType());
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus());
String beforeStatus = order.getStatus();
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(beforeStatus);
if (status == WorkOrderStatusEnum.COMPLETED || status == WorkOrderStatusEnum.CANCELLED) {
throw new IllegalStateException("已完成或已取消的工单不允许升级优先级");
}
@@ -139,8 +134,10 @@ public class ManualOrderActionFacade {
if (cmd.getReason() != null) {
payload.put(OrderAuditPayloadKeys.REASON, cmd.getReason());
}
String message = String.format("升级优先级 P%d → P%d",
oldPriority != null ? oldPriority : 2, cmd.getNewPriority());
String opName = cmd.getOperator().getOperatorName() != null
? cmd.getOperator().getOperatorName() : "操作人";
String message = String.format("%s 将优先级从 P%d 升级为 P%d",
opName, oldPriority != null ? oldPriority : 2, cmd.getNewPriority());
orderAuditService.record(order, ManualActionTypeEnum.MANUAL_UPGRADE_PRIORITY,
cmd.getOperator(), message, payload);
@@ -157,7 +154,8 @@ public class ManualOrderActionFacade {
public void cancel(CancelOrderCommand cmd) {
// 1. 查单 + 校验
OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType());
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus());
String beforeStatus = order.getStatus();
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(beforeStatus);
// 幂等
if (status == WorkOrderStatusEnum.CANCELLED) {
@@ -172,19 +170,23 @@ public class ManualOrderActionFacade {
OrderBusinessStrategy strategy = resolveStrategy(cmd.getBusinessType());
strategy.validateCancel(cmd, order);
// 3. 状态变更
orderLifecycleManager.cancelOrder(cmd.getOrderId(),
cmd.getOperator().getOperatorId(),
cmd.getOperator().getOperatorType(),
cmd.getReason());
// 3. 状态变更(走 transition 以透传 operatorName 到领域事件)
OrderTransitionRequest cancelRequest = OrderTransitionRequest.builder()
.orderId(cmd.getOrderId())
.targetStatus(WorkOrderStatusEnum.CANCELLED)
.operatorType(cmd.getOperator().getOperatorType())
.operatorId(cmd.getOperator().getOperatorId())
.operatorName(cmd.getOperator().getOperatorName())
.reason(cmd.getReason())
.assigneeId(order.getAssigneeId())
.build();
OrderTransitionResult cancelResult = orderLifecycleManager.transition(cancelRequest);
if (!cancelResult.isSuccess()) {
throw new IllegalStateException("手动取消失败: " + cancelResult.getMessage());
}
// 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. 条线后置
// 4. 条线后置
// 注:业务日志由生命周期事件 → 条线 EventListener 统一记录,此处不重复写
strategy.afterCancel(cmd, order);
log.info("[ManualOrderActionFacade] 手动取消完成: orderId={}", cmd.getOrderId());
@@ -196,7 +198,8 @@ public class ManualOrderActionFacade {
public void complete(CompleteOrderCommand cmd) {
// 1. 查单 + 校验
OpsOrderDO order = getOrderAndValidateType(cmd.getOrderId(), cmd.getBusinessType());
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(order.getStatus());
String beforeStatus = order.getStatus();
WorkOrderStatusEnum status = WorkOrderStatusEnum.valueOf(beforeStatus);
// 幂等
if (status == WorkOrderStatusEnum.COMPLETED) {
@@ -211,25 +214,27 @@ public class ManualOrderActionFacade {
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());
// 3. 状态变更(走 transition 以透传 operatorName 到领域事件)
String completeOpName = cmd.getOperator().getOperatorName() != null
? cmd.getOperator().getOperatorName() : "操作人";
String remark = cmd.getReason() != null ? cmd.getReason()
: String.format("%s 手动完成工单", completeOpName);
OrderTransitionRequest completeRequest = OrderTransitionRequest.builder()
.orderId(cmd.getOrderId())
.targetStatus(WorkOrderStatusEnum.COMPLETED)
.operatorType(cmd.getOperator().getOperatorType())
.operatorId(cmd.getOperator().getOperatorId())
.operatorName(cmd.getOperator().getOperatorName())
.reason(remark)
.assigneeId(order.getAssigneeId())
.build();
OrderTransitionResult completeResult = orderLifecycleManager.transition(completeRequest);
if (!completeResult.isSuccess()) {
throw new IllegalStateException("手动完单失败: " + completeResult.getMessage());
}
if (cmd.getResult() != null) {
payload.put(OrderAuditPayloadKeys.RESULT, cmd.getResult());
}
orderAuditService.record(order, ManualActionTypeEnum.MANUAL_COMPLETE,
cmd.getOperator(), remark, payload);
// 5. 条线后置
// 4. 条线后置
// 注:业务日志由生命周期事件 → 条线 EventListener 统一记录,此处不重复写
strategy.afterComplete(cmd, order);
log.info("[ManualOrderActionFacade] 手动完单完成: orderId={}", cmd.getOrderId());

View File

@@ -9,107 +9,52 @@ 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(审计检索)双写一致
* 用于 create / upgradePriority 这类不经过状态机的手动动作,
* 写入 ops_business_event_log。
* <p>
* personId 永远表示真实操作者,被操作目标放入 payload
*
* @author lzh
* ops_order_event 只由状态机写,此服务不写时间轴
* dispatch / cancel / complete 走生命周期 → 条线 EventListener
* 由 listener 统一记录业务日志,不在此服务重复写。
*/
@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);
OperatorContext operator,
String message, Map<String, Object> payload) {
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);
}
// 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);
}
eventLogRecorder.record(EventLogRecord.builder()
.module(LogModule.fromOrderType(order.getOrderType()))
.domain(EventDomain.AUDIT)
.eventType(mapToLogType(actionType).getCode())
.message(message)
.targetId(order.getId())
.targetType("order")
.personId(operator.getOperatorId())
.payload(auditPayload)
.build());
}
/**
* 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;