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:
lzh
2026-03-27 16:08:48 +08:00
parent 5d91097e75
commit 1d09a50643
5 changed files with 196 additions and 26 deletions

View File

@@ -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()

View File

@@ -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());
}
}

View File

@@ -45,4 +45,7 @@ public class SecurityOrderCreateReqDTO {
private String sourceType;
/** 操作人ID管理员手动创建时传入告警触发时为 null */
private Long operatorId;
}

View File

@@ -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查询安保扩展信息
*

View File

@@ -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;
}
}