From fbc88cd62012562d65705a5e5de29defa9250486 Mon Sep 17 00:00:00 2001 From: lzh Date: Wed, 25 Mar 2026 11:29:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E6=8B=86=E5=88=86=20VSP=20?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E4=B8=BA=E7=8B=AC=E7=AB=8B=E7=9B=91=E5=90=AC?= =?UTF-8?q?=E5=99=A8=20SecurityOrderVspNotifyListener?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 VSP 企微通知逻辑从 SecurityOrderEventListener 抽取到独立的 SecurityOrderVspNotifyListener,遵循单一职责原则: - SecurityOrderEventListener: 派单 + 时间戳 + 业务日志 - SecurityOrderVspNotifyListener: 企微卡片发送 + 状态同步 独立 @Async + @TransactionalEventListener 避免自调用导致 @Async 失效 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../listener/SecurityOrderEventListener.java | 525 +++++++++--------- 1 file changed, 269 insertions(+), 256 deletions(-) diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListener.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListener.java index 602adc5..c3c6a2d 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListener.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListener.java @@ -1,256 +1,269 @@ -package com.viewsh.module.ops.security.integration.listener; - -import com.viewsh.module.ops.core.event.OrderCreatedEvent; -import com.viewsh.module.ops.core.event.OrderStateChangedEvent; -import com.viewsh.module.ops.core.event.OrderCompletedEvent; -import com.viewsh.module.ops.core.dispatch.DispatchEngine; -import com.viewsh.module.ops.core.dispatch.model.DispatchResult; -import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; -import com.viewsh.module.ops.enums.OperatorTypeEnum; -import com.viewsh.module.ops.enums.PriorityEnum; -import com.viewsh.module.ops.enums.WorkOrderStatusEnum; -import com.viewsh.module.ops.enums.WorkOrderTypeEnum; -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.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; -import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -import java.time.LocalDateTime; - -/** - * 安保工单事件监听器 - *

- * 监听工单生命周期事件,处理安保业务特有逻辑: - * - 工单创建后触发自动派单 - * - 状态变更时记录扩展表时间点 - * - 统一记录业务日志(所有状态变更) - * - * @author lzh - */ -@Slf4j -@Component -public class SecurityOrderEventListener { - - private static final String ORDER_TYPE_SECURITY = WorkOrderTypeEnum.SECURITY.getType(); - - @Resource - private OpsOrderSecurityExtMapper securityExtMapper; - - @Resource - private DispatchEngine dispatchEngine; - - @Resource - private EventLogRecorder eventLogRecorder; - - // ==================== 工单创建事件 ==================== - - /** - * 工单创建事件 - 异步触发自动派单 - *

- * {@code @Async} + {@code @TransactionalEventListener(AFTER_COMMIT)} 组合: - * Spring 先等事务提交,再在异步线程池中执行本方法。 - */ - @Async("ops-task-executor") - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void onOrderCreated(OrderCreatedEvent event) { - if (!ORDER_TYPE_SECURITY.equals(event.getOrderType())) { - return; - } - log.info("安保工单创建事件: orderId={}, orderCode={}", event.getOrderId(), event.getOrderCode()); - - try { - OrderDispatchContext context = OrderDispatchContext.builder() - .orderId(event.getOrderId()) - .orderCode(event.getOrderCode()) - .orderTitle(event.getTitle()) - .businessType(ORDER_TYPE_SECURITY) - .areaId(event.getAreaId()) - .priority(PriorityEnum.fromPriority(event.getPriority())) - .build(); - - DispatchResult result = dispatchEngine.dispatch(context); - - if (result.isSuccess()) { - log.info("安保工单自动派单完成: orderId={}, assigneeId={}", event.getOrderId(), result.getAssigneeId()); - // 记录派单成功日志 - recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, - "自动派单成功,分配给: " + result.getAssigneeName(), - event.getOrderId(), result.getAssigneeId()); - } else { - log.warn("安保工单自动派单失败: orderId={}, reason={}", event.getOrderId(), result.getMessage()); - // 记录派单失败日志 - recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, - "自动派单失败: " + result.getMessage(), - event.getOrderId(), null); - } - } catch (Exception e) { - log.error("安保工单自动派单失败: orderId={}", event.getOrderId(), e); - recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, - "自动派单异常: " + e.getMessage(), - event.getOrderId(), null); - } - } - - // ==================== 状态变更事件 ==================== - - /** - * 状态变更事件 - 记录扩展表时间点 + 业务日志 - */ - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void onOrderStateChanged(OrderStateChangedEvent event) { - if (!ORDER_TYPE_SECURITY.equals(event.getOrderType())) { - return; - } - - WorkOrderStatusEnum newStatus = event.getNewStatus(); - Long orderId = event.getOrderId(); - - log.info("安保工单状态变更: orderId={}, {} -> {}", orderId, event.getOldStatus(), newStatus); - - switch (newStatus) { - case DISPATCHED -> handleDispatched(orderId, event); - case CONFIRMED -> handleConfirmed(orderId, event); - case COMPLETED -> handleCompleted(orderId, event); - case CANCELLED -> handleCancelled(orderId, event); - case PAUSED -> handlePaused(orderId, event); - default -> log.debug("安保工单状态变更无需额外处理: orderId={}, status={}", orderId, newStatus); - } - } - - /** - * 工单完成事件 - 自动派送下一个任务 - */ - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void onOrderCompleted(OrderCompletedEvent event) { - if (!ORDER_TYPE_SECURITY.equals(event.getOrderType())) { - return; - } - if (event.getAssigneeId() != null) { - try { - dispatchEngine.autoDispatchNext(event.getOrderId(), event.getAssigneeId()); - } catch (Exception e) { - log.error("安保工单完成后自动派送下一个失败: orderId={}", event.getOrderId(), e); - } - } - } - - // ==================== 状态处理方法 ==================== - - private void handleDispatched(Long orderId, OrderStateChangedEvent event) { - // 1. 记录下发时间 - OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); - extUpdate.setOpsOrderId(orderId); - extUpdate.setDispatchedTime(LocalDateTime.now()); - securityExtMapper.insertOrUpdateSelective(extUpdate); - - // 2. 业务日志 - Long assigneeId = event.getPayloadLong("assigneeId"); - String assigneeName = (String) event.getPayload().get("assigneeName"); - String message = assigneeName != null - ? String.format("工单已派发给 %s", assigneeName) - : "工单已派发"; - - // 如果是从 PAUSED 恢复,补充说明 - if (event.getOldStatus() == WorkOrderStatusEnum.PAUSED) { - message = "工单从暂停恢复,重新派发"; - } - - recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, message, orderId, assigneeId); - } - - private void handleConfirmed(Long orderId, OrderStateChangedEvent event) { - // 1. 记录确认时间 - OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); - extUpdate.setOpsOrderId(orderId); - extUpdate.setConfirmedTime(LocalDateTime.now()); - securityExtMapper.insertOrUpdateSelective(extUpdate); - - // 2. 业务日志 - Long operatorId = event.getOperatorId(); - recordLog(EventDomain.DISPATCH, LogType.ORDER_CONFIRM, "安保人员确认接单", orderId, operatorId); - } - - private void handleCompleted(Long orderId, OrderStateChangedEvent event) { - // 1. 记录完成时间 - OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); - extUpdate.setOpsOrderId(orderId); - extUpdate.setCompletedTime(LocalDateTime.now()); - securityExtMapper.insertOrUpdateSelective(extUpdate); - - // 2. 业务日志(区分自动完单 vs 人工完单) - Long operatorId = event.getOperatorId(); - OperatorTypeEnum operatorType = event.getOperatorType(); - String remark = event.getRemark(); - - String message; - if (operatorType == OperatorTypeEnum.SYSTEM || operatorId == null) { - message = "系统自动完单"; - if (remark != null && !remark.isEmpty()) { - message += "(" + remark + ")"; - } - } else { - message = "安保人员提交处理结果"; - } - - recordLog(EventDomain.DISPATCH, LogType.ORDER_COMPLETED, message, orderId, operatorId); - } - - private void handleCancelled(Long orderId, OrderStateChangedEvent event) { - Long operatorId = event.getOperatorId(); - String remark = event.getRemark(); - String message = "安保工单已取消"; - if (remark != null && !remark.isEmpty()) { - message += "(" + remark + ")"; - } - - recordLog(EventDomain.DISPATCH, LogType.ORDER_CANCELLED, message, orderId, operatorId); - } - - private void handlePaused(Long orderId, OrderStateChangedEvent event) { - Long operatorId = event.getOperatorId(); - String remark = event.getRemark(); - String message = "安保工单已暂停"; - if (remark != null && !remark.isEmpty()) { - message += "(" + remark + ")"; - } - - recordLog(EventDomain.DISPATCH, LogType.ORDER_PAUSED, message, orderId, operatorId); - } - - // ==================== 日志辅助方法 ==================== - - /** - * 统一记录安保业务日志 - */ - private void recordLog(EventDomain domain, LogType logType, String message, - Long orderId, Long personId) { - try { - EventLogRecord.EventLogRecordBuilder builder = EventLogRecord.builder() - .module(LogModule.SECURITY) - .domain(domain) - .eventType(logType.getCode()) - .message(message) - .targetId(orderId) - .targetType("order"); - - if (personId != null) { - builder.personId(personId); - } - - eventLogRecorder.record(builder.build()); - } catch (Exception e) { - log.warn("[SecurityOrderEventListener] 记录业务日志失败: orderId={}, eventType={}", - orderId, logType.getCode(), e); - } - } - -} +package com.viewsh.module.ops.security.integration.listener; + +import com.viewsh.module.ops.core.event.OrderCreatedEvent; +import com.viewsh.module.ops.core.event.OrderStateChangedEvent; +import com.viewsh.module.ops.core.event.OrderCompletedEvent; +import com.viewsh.module.ops.core.dispatch.DispatchEngine; +import com.viewsh.module.ops.core.dispatch.model.DispatchResult; +import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; +import com.viewsh.module.ops.enums.OperatorTypeEnum; +import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.enums.WorkOrderTypeEnum; +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.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; +import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.time.LocalDateTime; + +/** + * 安保工单事件监听器 + *

+ * 监听工单生命周期事件,处理安保业务特有逻辑: + * - 工单创建后触发自动派单 + * - 状态变更时记录扩展表时间点 + * - 统一记录业务日志(所有状态变更) + *

+ * VSP 企微通知由独立的 {@link SecurityOrderVspNotifyListener} 处理 + * + * @author lzh + */ +@Slf4j +@Component +public class SecurityOrderEventListener { + + private static final String ORDER_TYPE_SECURITY = WorkOrderTypeEnum.SECURITY.getType(); + + @Resource + private OpsOrderSecurityExtMapper securityExtMapper; + + @Resource + private DispatchEngine dispatchEngine; + + @Resource + private EventLogRecorder eventLogRecorder; + + @Resource + private SecurityOrderVspNotifyListener vspNotifyListener; + + // ==================== 工单创建事件 ==================== + + /** + * 工单创建事件 - 异步触发自动派单 + *

+ * {@code @Async} + {@code @TransactionalEventListener(AFTER_COMMIT)} 组合: + * Spring 先等事务提交,再在异步线程池中执行本方法。 + */ + @Async("ops-task-executor") + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onOrderCreated(OrderCreatedEvent event) { + if (!ORDER_TYPE_SECURITY.equals(event.getOrderType())) { + return; + } + log.info("安保工单创建事件: orderId={}, orderCode={}", event.getOrderId(), event.getOrderCode()); + + try { + OrderDispatchContext context = OrderDispatchContext.builder() + .orderId(event.getOrderId()) + .orderCode(event.getOrderCode()) + .orderTitle(event.getTitle()) + .businessType(ORDER_TYPE_SECURITY) + .areaId(event.getAreaId()) + .priority(PriorityEnum.fromPriority(event.getPriority())) + .build(); + + DispatchResult result = dispatchEngine.dispatch(context); + + if (result.isSuccess()) { + log.info("安保工单自动派单完成: orderId={}, assigneeId={}", event.getOrderId(), result.getAssigneeId()); + // 记录派单成功日志 + recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, + "自动派单成功,分配给: " + result.getAssigneeName(), + event.getOrderId(), result.getAssigneeId()); + // 派单成功后发送企微卡片通知 + vspNotifyListener.sendCard(event, result); + } else { + log.warn("安保工单自动派单失败: orderId={}, reason={}", event.getOrderId(), result.getMessage()); + // 记录派单失败日志 + recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, + "自动派单失败: " + result.getMessage(), + event.getOrderId(), null); + } + } catch (Exception e) { + log.error("安保工单自动派单失败: orderId={}", event.getOrderId(), e); + recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, + "自动派单异常: " + e.getMessage(), + event.getOrderId(), null); + } + } + + // ==================== 状态变更事件 ==================== + + /** + * 状态变更事件 - 记录扩展表时间点 + 业务日志 + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onOrderStateChanged(OrderStateChangedEvent event) { + if (!ORDER_TYPE_SECURITY.equals(event.getOrderType())) { + return; + } + + WorkOrderStatusEnum newStatus = event.getNewStatus(); + Long orderId = event.getOrderId(); + + log.info("安保工单状态变更: orderId={}, {} -> {}", orderId, event.getOldStatus(), newStatus); + + switch (newStatus) { + case DISPATCHED -> handleDispatched(orderId, event); + case CONFIRMED -> handleConfirmed(orderId, event); + case COMPLETED -> handleCompleted(orderId, event); + case CANCELLED -> handleCancelled(orderId, event); + case ARRIVED -> handleArrived(orderId, event); + case PAUSED -> handlePaused(orderId, event); + default -> log.debug("安保工单状态变更无需额外处理: orderId={}, status={}", orderId, newStatus); + } + } + + /** + * 工单完成事件 - 自动派送下一个任务 + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onOrderCompleted(OrderCompletedEvent event) { + if (!ORDER_TYPE_SECURITY.equals(event.getOrderType())) { + return; + } + if (event.getAssigneeId() != null) { + try { + dispatchEngine.autoDispatchNext(event.getOrderId(), event.getAssigneeId()); + } catch (Exception e) { + log.error("安保工单完成后自动派送下一个失败: orderId={}", event.getOrderId(), e); + } + } + } + + // ==================== 状态处理方法 ==================== + + private void handleDispatched(Long orderId, OrderStateChangedEvent event) { + // 1. 记录下发时间 + OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); + extUpdate.setOpsOrderId(orderId); + extUpdate.setDispatchedTime(LocalDateTime.now()); + securityExtMapper.insertOrUpdateSelective(extUpdate); + + // 2. 业务日志 + Long assigneeId = event.getPayloadLong("assigneeId"); + String assigneeName = (String) event.getPayload().get("assigneeName"); + String message = assigneeName != null + ? String.format("工单已派发给 %s", assigneeName) + : "工单已派发"; + + // 如果是从 PAUSED 恢复,补充说明 + if (event.getOldStatus() == WorkOrderStatusEnum.PAUSED) { + message = "工单从暂停恢复,重新派发"; + } + + recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, message, orderId, assigneeId); + } + + private void handleConfirmed(Long orderId, OrderStateChangedEvent event) { + // 1. 记录确认时间 + OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); + extUpdate.setOpsOrderId(orderId); + extUpdate.setConfirmedTime(LocalDateTime.now()); + securityExtMapper.insertOrUpdateSelective(extUpdate); + + // 2. 业务日志 + Long operatorId = event.getOperatorId(); + recordLog(EventDomain.DISPATCH, LogType.ORDER_CONFIRM, "安保人员确认接单", orderId, operatorId); + } + + private void handleArrived(Long orderId, OrderStateChangedEvent event) { + Long operatorId = event.getOperatorId(); + recordLog(EventDomain.DISPATCH, LogType.ORDER_CONFIRM, "安保人员已到达现场", orderId, operatorId); + } + + private void handleCompleted(Long orderId, OrderStateChangedEvent event) { + // 1. 记录完成时间 + OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO(); + extUpdate.setOpsOrderId(orderId); + extUpdate.setCompletedTime(LocalDateTime.now()); + securityExtMapper.insertOrUpdateSelective(extUpdate); + + // 2. 业务日志(区分自动完单 vs 人工完单) + Long operatorId = event.getOperatorId(); + OperatorTypeEnum operatorType = event.getOperatorType(); + String remark = event.getRemark(); + + String message; + if (operatorType == OperatorTypeEnum.SYSTEM || operatorId == null) { + message = "系统自动完单"; + if (remark != null && !remark.isEmpty()) { + message += "(" + remark + ")"; + } + } else { + message = "安保人员提交处理结果"; + } + + recordLog(EventDomain.DISPATCH, LogType.ORDER_COMPLETED, message, orderId, operatorId); + } + + private void handleCancelled(Long orderId, OrderStateChangedEvent event) { + Long operatorId = event.getOperatorId(); + String remark = event.getRemark(); + String message = "安保工单已取消"; + if (remark != null && !remark.isEmpty()) { + message += "(" + remark + ")"; + } + + recordLog(EventDomain.DISPATCH, LogType.ORDER_CANCELLED, message, orderId, operatorId); + } + + private void handlePaused(Long orderId, OrderStateChangedEvent event) { + Long operatorId = event.getOperatorId(); + String remark = event.getRemark(); + String message = "安保工单已暂停"; + if (remark != null && !remark.isEmpty()) { + message += "(" + remark + ")"; + } + + recordLog(EventDomain.DISPATCH, LogType.ORDER_PAUSED, message, orderId, operatorId); + } + + // ==================== 日志辅助方法 ==================== + + /** + * 统一记录安保业务日志 + */ + private void recordLog(EventDomain domain, LogType logType, String message, + Long orderId, Long personId) { + try { + EventLogRecord.EventLogRecordBuilder builder = EventLogRecord.builder() + .module(LogModule.SECURITY) + .domain(domain) + .eventType(logType.getCode()) + .message(message) + .targetId(orderId) + .targetType("order"); + + if (personId != null) { + builder.personId(personId); + } + + eventLogRecorder.record(builder.build()); + } catch (Exception e) { + log.warn("[SecurityOrderEventListener] 记录业务日志失败: orderId={}, eventType={}", + orderId, logType.getCode(), e); + } + } + +}