feat(security): 安保条线接入 ManualOrderActionFacade
SecurityOrderBusinessStrategy: - validateDispatch: 去除区域绑定强限制,允许跨区域派单,保留姓名回填 - afterUpgradePriority: 队列分数重算 SecurityOrderServiceImpl: - manualDispatch: resolveUser 一次查询同时拿姓名和手机号 - manualCancel/upgradePriority: 委托 facade,传入 operatorName - 手动创建走 OrderAuditService 审计(告警触发仅写业务日志) SecurityOrderEventListener: - 3 处 resolveOperatorName() 改为从 event.payload.operatorName 读取 - 移除 AdminUserApi 依赖,消除重复远程调用 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,7 @@ public class SecurityOrderCreateReqDTO {
|
||||
|
||||
private String sourceType;
|
||||
|
||||
/** 操作人ID(管理员手动创建时传入,告警触发时为 null) */
|
||||
private Long operatorId;
|
||||
|
||||
}
|
||||
|
||||
@@ -58,6 +58,40 @@ public interface SecurityOrderService {
|
||||
*/
|
||||
void falseAlarmOrder(Long orderId);
|
||||
|
||||
/**
|
||||
* 手动指定派单(管理员指定安保人员)
|
||||
* <p>
|
||||
* 绕过自动调度,直接将工单派给指定人员。
|
||||
* 工单状态:PENDING → DISPATCHED
|
||||
*
|
||||
* @param orderId 工单ID
|
||||
* @param assigneeId 指定安保人员ID
|
||||
* @param operatorId 操作人ID(管理员)
|
||||
* @param remark 备注
|
||||
*/
|
||||
void manualDispatch(Long orderId, Long assigneeId, Long operatorId, String remark);
|
||||
|
||||
/**
|
||||
* 手动升级优先级
|
||||
* <p>
|
||||
* 修改工单优先级,如果工单在队列中则重算队列分数。
|
||||
* 不触发重新调度,下次自动派发时自然优先。
|
||||
*
|
||||
* @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查询安保扩展信息
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user