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 d7e1d0a..082870d 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 @@ -6,6 +6,8 @@ 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.OrderActionSourceEnum; +import com.viewsh.module.ops.enums.OrderAuditPayloadKeys; import com.viewsh.module.ops.enums.OperatorTypeEnum; import com.viewsh.module.ops.enums.PriorityEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; @@ -196,17 +198,53 @@ public class SecurityOrderEventListener { extUpdate.setAssignedUserPhone(assigneePhone); securityExtMapper.insertOrUpdateSelective(extUpdate); - // 2. 业务日志 - String message = assigneeName != null - ? String.format("工单已派发给 %s", assigneeName) - : "工单已派发"; + // 2. 业务日志(区分手动派单 vs 自动派单 vs 暂停恢复) + OperatorTypeEnum operatorType = event.getOperatorType(); + Long operatorId = event.getOperatorId(); + String message; + Long logPersonId; // personId 表示真实操作者 - // 如果是从 PAUSED 恢复,补充说明 if (event.getOldStatus() == WorkOrderStatusEnum.PAUSED) { message = "工单从暂停恢复,重新派发"; + logPersonId = assigneeId; + } else if (operatorType == OperatorTypeEnum.ADMIN && operatorId != null) { + // 手动派单:personId 为管理员,被指派人员放入 payload + String operatorName = (String) event.getPayload().get("operatorName"); + String opLabel = operatorName != null ? operatorName : "操作人"; + String targetLabel = assigneeName != null ? assigneeName : "未知"; + message = String.format("%s 将工单派发给 %s", opLabel, targetLabel); + logPersonId = operatorId; + } else { + message = assigneeName != null + ? String.format("工单已派发给 %s", assigneeName) + : "工单已派发"; + logPersonId = assigneeId; } - recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, message, orderId, assigneeId); + // 构建日志(手动派单时 payload 包含被指派人员信息) + EventLogRecord.EventLogRecordBuilder logBuilder = EventLogRecord.builder() + .module(LogModule.SECURITY) + .domain(EventDomain.DISPATCH) + .eventType(LogType.ORDER_DISPATCHED.getCode()) + .message(message) + .targetId(orderId) + .targetType("order"); + if (logPersonId != null) { + logBuilder.personId(logPersonId); + } + if (operatorType == OperatorTypeEnum.ADMIN && operatorId != null && assigneeId != null) { + logBuilder.payload(java.util.Map.of( + OrderAuditPayloadKeys.ASSIGNEE_ID, assigneeId, + OrderAuditPayloadKeys.ASSIGNEE_NAME, assigneeName != null ? assigneeName : "", + OrderAuditPayloadKeys.MANUAL, true, + OrderAuditPayloadKeys.SOURCE, OrderActionSourceEnum.ADMIN_CONSOLE.getSource() + )); + } + try { + eventLogRecorder.record(logBuilder.build()); + } catch (Exception e) { + log.warn("[SecurityOrderEventListener] 记录派单业务日志失败: orderId={}", orderId, e); + } // 3. 工单推送时发送企微卡片通知(暂停恢复不重发,人员已知晓该工单) if (event.getOldStatus() != WorkOrderStatusEnum.PAUSED) { @@ -250,7 +288,10 @@ public class SecurityOrderEventListener { message += "(" + remark + ")"; } } else { - message = "安保人员提交处理结果"; + String operatorName = (String) event.getPayload().get("operatorName"); + message = operatorName != null + ? String.format("安保人员 %s 提交处理结果", operatorName) + : "安保人员提交处理结果"; } recordLog(EventDomain.DISPATCH, LogType.ORDER_COMPLETED, message, orderId, operatorId); @@ -261,14 +302,21 @@ public class SecurityOrderEventListener { OperatorTypeEnum operatorType = event.getOperatorType(); String remark = event.getRemark(); - // 区分取消来源 + // 区分取消来源 + 操作人 String source; if (operatorType == OperatorTypeEnum.SYSTEM) { source = "系统自动取消"; - } else if (operatorType == OperatorTypeEnum.ADMIN) { - source = "管理员手动取消"; } else { - source = "安保工单已取消"; + String operatorName = (String) event.getPayload().get("operatorName"); + if (operatorType == OperatorTypeEnum.ADMIN) { + source = operatorName != null + ? String.format("%s 取消了工单", operatorName) + : "手动取消工单"; + } else { + source = operatorName != null + ? String.format("%s 取消工单", operatorName) + : "安保工单已取消"; + } } String message = remark != null && !remark.isEmpty() diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/manual/SecurityOrderBusinessStrategy.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/manual/SecurityOrderBusinessStrategy.java index 82f0d88..68c7dd5 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/manual/SecurityOrderBusinessStrategy.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/manual/SecurityOrderBusinessStrategy.java @@ -39,14 +39,10 @@ public class SecurityOrderBusinessStrategy implements OrderBusinessStrategy { @Override public void validateDispatch(DispatchOrderCommand cmd, OpsOrderDO order) { - // 校验目标安保人员:必须绑定到工单所属区域且已启用 + // 尝试从区域绑定记录回填姓名(不做区域限制,允许跨区域派单) OpsAreaSecurityUserDO binding = areaSecurityUserMapper.selectByAreaIdAndUserId( order.getAreaId(), cmd.getAssigneeId()); - if (binding == null || !Boolean.TRUE.equals(binding.getEnabled())) { - throw exception(SECURITY_ASSIGNEE_NOT_BOUND_TO_AREA); - } - // 使用绑定记录中的冗余姓名 - if (cmd.getAssigneeName() == null && binding.getUserName() != null) { + if (binding != null && cmd.getAssigneeName() == null && binding.getUserName() != null) { cmd.setAssigneeName(binding.getUserName()); } } diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCreateReqDTO.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCreateReqDTO.java index bb065ff..b4e9650 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCreateReqDTO.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderCreateReqDTO.java @@ -45,4 +45,7 @@ public class SecurityOrderCreateReqDTO { private String sourceType; + /** 操作人ID(管理员手动创建时传入,告警触发时为 null) */ + private Long operatorId; + } diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java index d04b45c..b39a251 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderService.java @@ -58,6 +58,40 @@ public interface SecurityOrderService { */ void falseAlarmOrder(Long orderId); + /** + * 手动指定派单(管理员指定安保人员) + *

+ * 绕过自动调度,直接将工单派给指定人员。 + * 工单状态:PENDING → DISPATCHED + * + * @param orderId 工单ID + * @param assigneeId 指定安保人员ID + * @param operatorId 操作人ID(管理员) + * @param remark 备注 + */ + void manualDispatch(Long orderId, Long assigneeId, Long operatorId, String remark); + + /** + * 手动升级优先级 + *

+ * 修改工单优先级,如果工单在队列中则重算队列分数。 + * 不触发重新调度,下次自动派发时自然优先。 + * + * @param orderId 工单ID + * @param newPriority 新优先级(0=P0, 1=P1, 2=P2) + * @param operatorId 操作人ID(管理员) + */ + void upgradePriority(Long orderId, Integer newPriority, Long operatorId); + + /** + * 手动取消工单(管理员操作) + * + * @param orderId 工单ID + * @param reason 取消原因 + * @param operatorId 操作人ID(管理员) + */ + void manualCancel(Long orderId, String reason, Long operatorId); + /** * 根据工单ID查询安保扩展信息 * diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java index 079f048..cbd9e27 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceImpl.java @@ -20,11 +20,19 @@ 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.area.OpsAreaSecurityUserDO; import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; +import com.viewsh.module.ops.security.dal.mysql.area.OpsAreaSecurityUserMapper; import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; 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.ManualOrderActionFacade; +import com.viewsh.module.ops.core.manual.audit.OrderAuditService; +import com.viewsh.module.ops.core.manual.model.*; +import com.viewsh.module.ops.enums.ManualActionTypeEnum; +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; @@ -48,6 +56,9 @@ public class SecurityOrderServiceImpl implements SecurityOrderService { @Resource private OpsOrderSecurityExtMapper securityExtMapper; + @Resource + private OpsAreaSecurityUserMapper areaSecurityUserMapper; + @Resource private OrderIdGenerator orderIdGenerator; @@ -69,6 +80,18 @@ public class SecurityOrderServiceImpl implements SecurityOrderService { @Resource private EventLogRecorder eventLogRecorder; + @Resource + private com.viewsh.module.ops.api.queue.OrderQueueService orderQueueService; + + @Resource + private AdminUserApi adminUserApi; + + @Resource + private ManualOrderActionFacade manualOrderActionFacade; + + @Resource + private OrderAuditService orderAuditService; + private static final String BUSINESS_TYPE = WorkOrderTypeEnum.SECURITY.getType(); @Override @@ -128,15 +151,26 @@ public class SecurityOrderServiceImpl implements SecurityOrderService { .build(); orderEventPublisher.publishOrderCreated(event); - // 6. 记录业务日志 - eventLogRecorder.record(EventLogRecord.builder() - .module(LogModule.SECURITY) - .domain(EventDomain.DISPATCH) - .eventType(LogType.ORDER_CREATED.getCode()) - .message("安保工单创建") - .targetId(orderId) - .targetType("order") - .build()); + // 6. 记录审计日志(区分手动创建 vs 告警触发) + if (createReq.getOperatorId() != null) { + // 手动创建:记录审计日志(ops_business_event_log) + String operatorName = resolveUserName(createReq.getOperatorId()); + String createMessage = operatorName != null + ? String.format("%s 创建了安保工单", operatorName) + : "手动创建安保工单"; + orderAuditService.record(order, ManualActionTypeEnum.MANUAL_CREATE, + OperatorContext.ofAdmin(createReq.getOperatorId(), operatorName), createMessage, null); + } else { + // 告警触发:仅业务日志 + eventLogRecorder.record(EventLogRecord.builder() + .module(LogModule.SECURITY) + .domain(EventDomain.DISPATCH) + .eventType(LogType.ORDER_CREATED.getCode()) + .message("告警触发自动创建安保工单") + .targetId(orderId) + .targetType("order") + .build()); + } log.info("创建安保工单成功: orderId={}, orderCode={}, alarmId={}, areaId={}", orderId, orderCode, createReq.getAlarmId(), createReq.getAreaId()); @@ -239,6 +273,41 @@ public class SecurityOrderServiceImpl implements SecurityOrderService { log.info("安保工单人工完单: orderId={}", req.getOrderId()); } + @Override + public void manualDispatch(Long orderId, Long assigneeId, Long operatorId, String remark) { + // 查一次 assignee 信息,同时拿到姓名和手机号 + AdminUserRespDTO assignee = resolveUser(assigneeId); + manualOrderActionFacade.dispatch(DispatchOrderCommand.builder() + .businessType(BUSINESS_TYPE) + .orderId(orderId) + .operator(OperatorContext.ofAdmin(operatorId, resolveUserName(operatorId))) + .assigneeId(assigneeId) + .assigneeName(assignee != null ? assignee.getNickname() : null) + .assigneePhone(assignee != null ? assignee.getMobile() : null) + .reason(remark) + .build()); + } + + @Override + public void upgradePriority(Long orderId, Integer newPriority, Long operatorId) { + manualOrderActionFacade.upgradePriority(UpgradePriorityCommand.builder() + .businessType(BUSINESS_TYPE) + .orderId(orderId) + .operator(OperatorContext.ofAdmin(operatorId, resolveUserName(operatorId))) + .newPriority(newPriority) + .build()); + } + + @Override + public void manualCancel(Long orderId, String reason, Long operatorId) { + manualOrderActionFacade.cancel(CancelOrderCommand.builder() + .businessType(BUSINESS_TYPE) + .orderId(orderId) + .operator(OperatorContext.ofAdmin(operatorId, resolveUserName(operatorId))) + .reason(reason) + .build()); + } + @Override public OpsOrderSecurityExtDO getSecurityExt(Long opsOrderId) { return securityExtMapper.selectByOpsOrderId(opsOrderId); @@ -275,4 +344,24 @@ public class SecurityOrderServiceImpl implements SecurityOrderService { return assignedUserId; } + /** + * 根据用户 ID 查询姓名,查询失败返回 null(不影响主流程) + */ + private AdminUserRespDTO resolveUser(Long userId) { + if (userId == null) { + return null; + } + try { + return adminUserApi.getUser(userId).getCheckedData(); + } catch (Exception e) { + log.debug("查询用户信息失败: userId={}", userId); + return null; + } + } + + private String resolveUserName(Long userId) { + AdminUserRespDTO user = resolveUser(userId); + return user != null ? user.getNickname() : null; + } + }