diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderTransitionAttemptedEvent.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderTransitionAttemptedEvent.java new file mode 100644 index 00000000..6c3f112b --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderTransitionAttemptedEvent.java @@ -0,0 +1,85 @@ +package com.viewsh.module.ops.core.event; + +import com.viewsh.module.ops.core.lifecycle.model.TransitionErrorCode; +import com.viewsh.module.ops.enums.OperatorTypeEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 工单状态转换"尝试"领域事件 + *
+ * 与 {@link OrderStateChangedEvent} 的区别: + *
+ * 注意:这是发布瞬间的判断;如果后续 handler 抛异常导致整个事务 rollback, + * 监听器在 {@code AFTER_ROLLBACK} 阶段应强制将其视为失败。 + */ + private boolean success; + + /** 失败错误码(success=false 时有值) */ + private TransitionErrorCode errorCode; + + /** 失败原因(简要消息,success=false 时有值) */ + private String errorMessage; + + /** 异常摘要(success=false 且存在异常时有值,只保留 class + message,不带堆栈) */ + private String causeSummary; + + /** 事件时间 */ + private LocalDateTime attemptedAt; +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManagerImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManagerImpl.java index d9d4e104..e9dce59b 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManagerImpl.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManagerImpl.java @@ -1,5 +1,6 @@ package com.viewsh.module.ops.core.lifecycle; +import com.viewsh.module.ops.core.event.OrderTransitionAttemptedEvent; import com.viewsh.module.ops.core.lifecycle.handler.EventPublishHandler; import com.viewsh.module.ops.core.lifecycle.handler.QueueSyncHandler; import com.viewsh.module.ops.core.lifecycle.handler.StateTransitionHandler; @@ -8,6 +9,9 @@ import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest; import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionResult; import com.viewsh.module.ops.core.lifecycle.model.TransitionContext; import com.viewsh.module.ops.core.lifecycle.model.TransitionErrorCode; +import org.springframework.context.ApplicationEventPublisher; + +import java.time.LocalDateTime; import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.enums.OperatorTypeEnum; @@ -63,6 +67,9 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { @Resource private EventLogRecorder eventLogRecorder; + @Resource + private ApplicationEventPublisher applicationEventPublisher; + /** * 责任链处理器 */ @@ -102,10 +109,15 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { // 4. 检查结果 if (context.hasError()) { log.error("状态转换失败: orderId={}, error={}", order.getId(), context.getErrorMessage()); + publishAttempt(order, oldStatus, request, false, + TransitionErrorCode.INVALID_TRANSITION, + context.getErrorMessage(), + summarizeThrowable(context.getCause())); return OrderTransitionResult.fail(order.getId(), context.getErrorMessage()); } log.info("状态转换成功: orderId={}, {} -> {}", order.getId(), oldStatus, request.getTargetStatus()); + publishAttempt(order, oldStatus, request, true, null, null, null); return OrderTransitionResult.builder() .success(true) .orderId(order.getId()) @@ -153,12 +165,21 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { request.getAssigneeId(), request.getOrderId()); if (!activeOrders.isEmpty()) { OpsOrderDO head = activeOrders.get(0); + String msg = "执行人已有活跃工单: orderId=" + head.getId() + ", status=" + head.getStatus(); log.warn("派发被拒:执行人已有活跃工单: assigneeId={}, requestOrderId={}, activeCount={}, sampleOrderId={}, sampleStatus={}", request.getAssigneeId(), request.getOrderId(), activeOrders.size(), head.getId(), head.getStatus()); + + // 审计:记录"派发被拒"尝试,AFTER_COMMIT 监听器会写 bus_log + OpsOrderDO subject = opsOrderMapper.selectById(request.getOrderId()); + WorkOrderStatusEnum fromStatus = subject != null + ? WorkOrderStatusEnum.valueOf(subject.getStatus()) : null; + publishAttempt(subject != null ? subject : head, fromStatus, request, false, + TransitionErrorCode.ASSIGNEE_HAS_ACTIVE_ORDER, msg, null); + return OrderTransitionResult.fail( request.getOrderId(), - "执行人已有活跃工单: orderId=" + head.getId() + ", status=" + head.getStatus(), + msg, TransitionErrorCode.ASSIGNEE_HAS_ACTIVE_ORDER); } } @@ -430,4 +451,49 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { || WorkOrderStatusEnum.ARRIVED == status; } + /** + * 发布状态转换尝试事件,覆盖成功、普通失败、并发冲突三种情况。 + * 订阅方 {@code OrderTransitionAuditListener} 在 AFTER_COMMIT/AFTER_ROLLBACK + * 阶段落 bus_log,保证事务回滚不断链。 + */ + private void publishAttempt(OpsOrderDO order, WorkOrderStatusEnum fromStatus, + OrderTransitionRequest request, boolean success, + TransitionErrorCode errorCode, String errorMessage, + String causeSummary) { + try { + OrderTransitionAttemptedEvent event = OrderTransitionAttemptedEvent.builder() + .orderId(order != null ? order.getId() : request.getOrderId()) + .orderType(order != null ? order.getOrderType() : null) + .orderCode(order != null ? order.getOrderCode() : null) + .fromStatus(fromStatus) + .targetStatus(request.getTargetStatus()) + .assigneeId(request.getAssigneeId()) + .operatorType(request.getOperatorType()) + .operatorId(request.getOperatorId()) + .reason(request.getReason()) + .success(success) + .errorCode(errorCode) + .errorMessage(errorMessage) + .causeSummary(causeSummary) + .attemptedAt(LocalDateTime.now()) + .build(); + applicationEventPublisher.publishEvent(event); + } catch (Exception e) { + // 审计事件发布失败不应影响主流程 + log.error("发布转换尝试事件失败: orderId={}, targetStatus={}", + request.getOrderId(), request.getTargetStatus(), e); + } + } + + /** + * 摘要异常:只保留类名 + message,不带堆栈,防止 bus_log 爆炸。 + */ + private String summarizeThrowable(Throwable t) { + if (t == null) { + return null; + } + String msg = t.getMessage(); + return t.getClass().getSimpleName() + (msg != null ? ": " + msg : ""); + } + } diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/audit/OrderTransitionAuditListener.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/audit/OrderTransitionAuditListener.java new file mode 100644 index 00000000..4d6d13ca --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/audit/OrderTransitionAuditListener.java @@ -0,0 +1,174 @@ +package com.viewsh.module.ops.core.lifecycle.audit; + +import com.viewsh.module.ops.core.event.OrderTransitionAttemptedEvent; +import com.viewsh.module.ops.core.lifecycle.model.TransitionErrorCode; +import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; +import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel; +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 jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.util.HashMap; +import java.util.Map; + +/** + * 工单状态转换尝试审计监听器。 + *
+ * 闭环设计: + *
+ * 字段归位: + *
+ * fallbackExecution=true:在无事务上下文时也执行(如测试、跨线程补写场景)。 + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) + public void onAfterCommit(OrderTransitionAttemptedEvent event) { + try { + eventLogRecorder.recordSync(toRecord(event, /*rolledBack=*/false)); + } catch (Exception e) { + log.error("[TransitionAudit] AFTER_COMMIT 写 bus_log 失败: orderId={}, success={}, errorCode={}", + event.getOrderId(), event.isSuccess(), event.getErrorCode(), e); + } + } + + /** + * 主事务已回滚:无论事件里声称 success 与否,这次"尝试"都**实际未落库**。 + * 必须开独立事务写 bus_log,否则日志也会因同事务回滚而丢失。 + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) + public void onAfterRollback(OrderTransitionAttemptedEvent event) { + writeRollbackAudit(event); + } + + /** + * 独立事务写入失败审计。 + *
+ * AFTER_ROLLBACK 触发时主事务已结束,此处即便用 REQUIRES_NEW 也不会遇到嵌套锁;
+ * 失败本身只是单行 insert,不再对其他表加锁。
+ */
+ @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+ public void writeRollbackAudit(OrderTransitionAttemptedEvent event) {
+ try {
+ eventLogRecorder.recordSync(toRecord(event, /*rolledBack=*/true));
+ } catch (Exception e) {
+ log.error("[TransitionAudit] AFTER_ROLLBACK 写 bus_log 失败: orderId={}, targetStatus={}",
+ event.getOrderId(), event.getTargetStatus(), e);
+ }
+ }
+
+ // ==================== 私有映射方法 ====================
+
+ private EventLogRecord toRecord(OrderTransitionAttemptedEvent event, boolean rolledBack) {
+ // rolledBack=true 时强制视为失败:即便发布时声明 success=true,
+ // 事务 rollback 说明写入未真正生效。
+ boolean success = event.isSuccess() && !rolledBack;
+
+ EventLevel level = success ? EventLevel.INFO
+ : (event.getErrorCode() == TransitionErrorCode.ASSIGNEE_HAS_ACTIVE_ORDER
+ ? EventLevel.WARN : EventLevel.ERROR);
+
+ String eventTypeCode = resolveEventTypeCode(event, success, rolledBack);
+
+ Map
+ * 需要确保日志真正落库的场景使用(如 AFTER_COMMIT 审计、事务回滚场景补写)。
+ * 调用方负责事务边界:本方法内部不开启事务,MyBatis 的 insert 会按当前线程的
+ * 事务上下文执行;若无活跃事务则自动单条提交。
+ *
+ * @param record 日志记录
+ */
+ void recordSync(EventLogRecord record);
+
// ==================== 便捷方法:按级别记录 ====================
/**
diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/recorder/EventLogRecorderImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/recorder/EventLogRecorderImpl.java
index ea12055b..92bab9f0 100644
--- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/recorder/EventLogRecorderImpl.java
+++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/recorder/EventLogRecorderImpl.java
@@ -58,6 +58,7 @@ public class EventLogRecorderImpl implements EventLogRecorder {
*
* 用于需要确认日志写入成功的场景(如测试、关键业务)
*/
+ @Override
public void recordSync(EventLogRecord record) {
doRecord(record);
}